diff --git a/Classes/Compiler/AnchorMenuCompiler.php b/Classes/Compiler/AnchorMenuCompiler.php new file mode 100644 index 0000000..a2142ec --- /dev/null +++ b/Classes/Compiler/AnchorMenuCompiler.php @@ -0,0 +1,65 @@ +generateCacheIdentifierForMenu('anchor', $configuration); + $excludePages = $this->parseStdWrap($configuration['excludePages'] ?? '', $configuration['excludePages.'] ?? []); + $configuration['excludePages'] = $excludePages; + $depth = (int)$contentObjectRenderer->stdWrap($configuration['depth'] ?? 1, $configuration['depth.'] ?? []); + + + $pageIds = $contentObjectRenderer->stdWrap($configuration['pages'] ?? $this->getPageIds($this->menuRepository->getSubPagesOfPage($this->getCurrentSite()->getRootPageId(), $depth, $configuration)), $configuration['pages.'] ?? []); + $pageIds = GeneralUtility::intExplode(',', (string)$pageIds); + + $cacheIdentifier .= '-' . substr(md5(json_encode([$pageIds])), 0, 10); + + return $this->cache->get($cacheIdentifier, function () use ($configuration, $pageIds) { + $pages = []; + foreach ($pageIds as $pageId) { + $page = $this->menuRepository->getPage($pageId, $configuration); + $links = $this->menuRepository->getAnchorMenu($pageId, $configuration); + $page["anchors"] = $links; + if (!empty($page)) { + $pages[$pageId] = $page; + } + } + return $pages; + }); + } + + /** + * Get page ids from rootline + depth + * + * @param $pageArr + * @return string + */ + public function getPageIds($pageArr) + { + $pageIds = []; + foreach ($pageArr as $page) { + $pageIds[] = $page["uid"]; + } + return implode(",", $pageIds); + } +} diff --git a/Classes/ContentObject/AnchorMenuContentObject.php b/Classes/ContentObject/AnchorMenuContentObject.php new file mode 100644 index 0000000..fa17e54 --- /dev/null +++ b/Classes/ContentObject/AnchorMenuContentObject.php @@ -0,0 +1,58 @@ +getMajorVersion() < 12) { + parent::__construct($cObj); + } + $this->anchorMenuCompiler = (GeneralUtility::makeInstance(ContentObjectServiceContainer::class))->getAnchorMenuCompiler(); + } + + /** + * @param array $conf + * @return string + */ + public function render($conf = []) + { + $pages = $this->anchorMenuCompiler->compile($this->cObj, $conf); + $content = $this->renderItems($pages, $conf); + return $this->cObj->stdWrap($content, $conf); + } + + protected function renderItems(array $pages, array $conf): string + { + $content = ''; + $cObjForItems = GeneralUtility::makeInstance(ContentObjectRenderer::class); + foreach ($pages as $page) { + PageStateMarker::markStates($page); + $cObjForItems->start($page, 'pages'); + $content .= $cObjForItems->cObjGetSingle($conf['renderObj'] ?? '', $conf['renderObj.'] ?? []); + } + return $content; + } +} diff --git a/Classes/ContentObject/ContentObjectServiceContainer.php b/Classes/ContentObject/ContentObjectServiceContainer.php index 5f595f8..bd72c91 100644 --- a/Classes/ContentObject/ContentObjectServiceContainer.php +++ b/Classes/ContentObject/ContentObjectServiceContainer.php @@ -14,6 +14,7 @@ use B13\Menus\Compiler\LanguageMenuCompiler; use B13\Menus\Compiler\ListMenuCompiler; +use B13\Menus\Compiler\AnchorMenuCompiler; use B13\Menus\Compiler\TreeMenuCompiler; use B13\Menus\Domain\Repository\MenuRepository; use TYPO3\CMS\Core\SingletonInterface; @@ -26,17 +27,21 @@ class ContentObjectServiceContainer implements SingletonInterface protected LanguageMenuCompiler $languageMenuCompiler; protected MenuRepository $menuRepository; protected ListMenuCompiler $listMenuCompiler; + protected AnchorMenuCompiler $anchorMenuCompiler; protected TreeMenuCompiler $treeMenuCompiler; public function __construct( LanguageMenuCompiler $languageMenuCompiler, - MenuRepository $menuRepository, - ListMenuCompiler $listMenuCompiler, - TreeMenuCompiler $treeMenuCompiler - ) { + MenuRepository $menuRepository, + ListMenuCompiler $listMenuCompiler, + AnchorMenuCompiler $anchorMenuCompiler, + TreeMenuCompiler $treeMenuCompiler + ) + { $this->languageMenuCompiler = $languageMenuCompiler; $this->menuRepository = $menuRepository; $this->listMenuCompiler = $listMenuCompiler; + $this->anchorMenuCompiler = $anchorMenuCompiler; $this->treeMenuCompiler = $treeMenuCompiler; } @@ -55,6 +60,11 @@ public function getListMenuCompiler(): ListMenuCompiler return $this->listMenuCompiler; } + public function getAnchorMenuCompiler(): AnchorMenuCompiler + { + return $this->anchorMenuCompiler; + } + public function getTreeMenuCompiler(): TreeMenuCompiler { return $this->treeMenuCompiler; diff --git a/Classes/DataProcessing/AnchorMenu.php b/Classes/DataProcessing/AnchorMenu.php new file mode 100644 index 0000000..cdc5ec6 --- /dev/null +++ b/Classes/DataProcessing/AnchorMenu.php @@ -0,0 +1,55 @@ +anchorMenuCompiler = $anchorMenuCompiler; + parent::__construct($contentDataProcessor); + } + + /** + * @inheritDoc + */ + public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData) + { + if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) { + return $processedData; + } + + $pages = $this->anchorMenuCompiler->compile($cObj, $processorConfiguration); + + foreach ($pages as &$page) { + PageStateMarker::markStates($page); + } + foreach ($pages as &$page) { + $this->processAdditionalDataProcessors($page, $processorConfiguration); + } + $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration); + $processedData[$targetVariableName] = $pages; + + return $processedData; + } +} diff --git a/Classes/Domain/Repository/MenuRepository.php b/Classes/Domain/Repository/MenuRepository.php index 86425c4..1b35a80 100644 --- a/Classes/Domain/Repository/MenuRepository.php +++ b/Classes/Domain/Repository/MenuRepository.php @@ -1,6 +1,7 @@ context = $context; $this->pageRepository = $pageRepository; $this->eventDispatcher = $eventDispatcher; + $this->connectionPool = $connectionPool; + } + + public function getAnchorMenu(int $pageId, array $configuration): array + { + //@TODO add configuration options like e.g. filter + + + //Get the queryBuilder for tt_content + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('tt_content'); + $result = $queryBuilder + ->select("header", "tx_menus_anchor_nav_title") + ->from("tt_content") + ->where($queryBuilder->expr()->eq("pid", $queryBuilder->createNamedParameter($pageId))) + ->andWhere($queryBuilder->expr()->eq("deleted", 0)) + ->andWhere($queryBuilder->expr()->eq("hidden", 0)) + ->andWhere($queryBuilder->expr()->eq("tx_menus_show_in_anchor_menu", 1)) + ->andWhere($queryBuilder->expr()->eq("CType", $queryBuilder->createNamedParameter("header"))) + ->orderBy("sorting") + ->executeQuery(); + + $menu = []; + while ($row = $result->fetchAssociative()) { + $tmp = []; + $navTitle = $row["tx_menus_anchor_nav_title"]; + + $tmp["title"] = $row["header"]; + $tmp["id"] = HelperFunctions::getAnchorId($row["header"]); + $tmp["nav_title"] = $navTitle; + $menu[] = $tmp; + } + + return $menu; } + public function getBreadcrumbsMenu(array $originalRootLine, array $configuration): array { $pages = []; @@ -145,6 +184,7 @@ public function getSubPagesOfPage(int $pageId, int $depth, array $configuration) 'AND doktype NOT IN (' . implode(',', $excludedDoktypes) . ') ' . $whereClause, false ); + /** @var LanguageAspect $languageAspect */ $languageAspect = $this->context->getAspect('language'); foreach ($pageTree as $k => &$page) { @@ -153,7 +193,7 @@ public function getSubPagesOfPage(int $pageId, int $depth, array $configuration) continue; } if ($depth > 0) { - $page['subpages'] = $this->getSubPagesOfPage((int)$page['uid'], $depth-1, $configuration); + $page['subpages'] = $this->getSubPagesOfPage((int)$page['uid'], $depth - 1, $configuration); } $this->populateAdditionalKeysForPage($page); } diff --git a/Classes/Helpers/HelperFunctions.php b/Classes/Helpers/HelperFunctions.php new file mode 100644 index 0000000..16c5ae8 --- /dev/null +++ b/Classes/Helpers/HelperFunctions.php @@ -0,0 +1,24 @@ +registerArgument('title', 'string', 'The string which should be converted to be used as an html identifier', true); + } + + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) + { + $title = $renderChildrenClosure(); + return HelperFunctions::getAnchorId($title); + } + +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index cf0247c..f296226 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -13,6 +13,8 @@ services: public: true B13\Menus\DataProcessing\ListMenu: public: true + B13\Menus\DataProcessing\AnchorMenu: + public: true B13\Menus\DataProcessing\TreeMenu: public: true B13\Menus\ContentObject\ContentObjectServiceContainer: @@ -31,6 +33,10 @@ services: tags: - name: frontend.contentobject identifier: 'LISTMENU' + B13\Menus\ContentObject\AnchorMenuContentObject: + tags: + - name: frontend.contentobject + identifier: 'ANCHORMENU' B13\Menus\ContentObject\LanguageMenuContentObject: tags: - name: frontend.contentobject diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php new file mode 100644 index 0000000..bbb334a --- /dev/null +++ b/Configuration/TCA/Overrides/tt_content.php @@ -0,0 +1,48 @@ + [ + 'exclude' => 0, + 'label' => 'Nav Title', + 'config' => [ + 'type' => 'input', + 'renderType' => 'input', + 'size' => 50, + 'max' => 100, + 'eval' => 'trim', + ], + ], + 'tx_menus_show_in_anchor_menu' => [ + 'label' => 'Show in anchor menu', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 0, + ], + ], + ], +); + + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( + 'tt_content', + 'tx_menus_anchor_nav_title', + 'header', + 'after:header' +); + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( + 'tt_content', + 'tx_menus_show_in_anchor_menu', + 'header', + 'after:header' +); + + + diff --git a/README.md b/README.md index 986f7d4..43b10a8 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ ## Introduction TYPO3 CMS is known for handling large websites with lots of content. TYPO3 Core provides several ways to build -navigation / menus in a very flexible way. However, generating menus has been a tedious issue in most of our -large-scale projects. With TYPO3 v9, the performance of generating menus improved when it comes to URL generation, -but a few conceptual issues within linking and menu generation still exist: +navigation / menus in a very flexible way. However, generating menus has been a tedious issue in most of our large-scale +projects. With TYPO3 v9, the performance of generating menus improved when it comes to URL generation, but a few +conceptual issues within linking and menu generation still exist: 1. All logic relies on HMENU @@ -17,32 +17,31 @@ but a few conceptual issues within linking and menu generation still exist: 2. HMENU saves states for each page HMENU offers the possibility to define A LOT of states ("active", "current", "has children"). This information is - different for each page - obviously - which is then cached in a separate cache entry in `cache_hash` - making - the cache entries fairly large even though we do not use states. + different for each page - obviously - which is then cached in a separate cache entry in `cache_hash` - making the + cache entries fairly large even though we do not use states. - We use `expAll` (expand all subpages for all other pages as well) which makes the requests to the pages - enormously large. + We use `expAll` (expand all subpages for all other pages as well) which makes the requests to the pages enormously + large. 3. HMENU has a cryptic syntax for "special" menus - Nowadays, it is fairly common to build menus for footer navigation, mega-menus, sitemap-like menus for an additional - sidebar. Using "special." for language menus, for "directories" or just a simple list of pages, seems rather complex. - + Nowadays, it is fairly common to build menus for footer navigation, mega-menus, sitemap-like menus for an additional + sidebar. Using "special." for language menus, for "directories" or just a simple list of pages, seems rather complex. This extension tries to overcome these pitfalls by - * building menus once, then caches the results and afterwards applying active states (reduce amount of cached data). - This is especially important for Tree-based menus, - * introducing new cObjects and DataProcessors for the specific use cases making them more understandable for - non-TYPO3-Gurus. + +* building menus once, then caches the results and afterwards applying active states (reduce amount of cached data). + This is especially important for Tree-based menus, +* introducing new cObjects and DataProcessors for the specific use cases making them more understandable for + non-TYPO3-Gurus. ## Installation & Requirements Use `composer req b13/menus` or install it via TYPO3's Extension Manager from the [TYPO3 Extension Repository](https://extensions.typo3.org) using the extension key `menus`. -You need TYPO3 v9 with Site Handling for this extension to work. If your project supports mount points, -this is not implemented. In addition, pages to access restricted pages (even though no access exists) are not yet -considered. +You need TYPO3 v9 with Site Handling for this extension to work. If your project supports mount points, this is not +implemented. In addition, pages to access restricted pages (even though no access exists) are not yet considered. ## Features @@ -51,7 +50,8 @@ The extension ships TypoScript cObjects and TypoScript DataProcessors for Fluid- ### Common Options for all menus * excludePages - a list of page IDs (and their subpages if Tree Menu or Breadcrumbs is used) to exclude from the page -* excludeDoktypes - a list of doktypes that are not rendered. BE_USER_SECTIONs are excluded by default. SYS_FOLDERs are queried (for subpages etc) but never rendered. +* excludeDoktypes - a list of doktypes that are not rendered. BE_USER_SECTIONs are excluded by default. SYS_FOLDERs are + queried (for subpages etc) but never rendered. * includeNotInMenu - include pages with nav_hide set to 1, instead of ignoring them ### Common options for items @@ -188,7 +188,6 @@ Usage in Fluid: - ### Breadcrumb Menu (a.k.a. Rootline Menu) page.10 = BREADCRUMBS @@ -200,7 +199,6 @@ Usage in Fluid: page.10.renderObj.typolink.parameter.data = field:uid page.10.renderObj.wrap =