From 8e04b9ad9f83857c6c654d02f79e3add20c4a1b4 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Mon, 16 Oct 2023 22:37:01 +0200 Subject: [PATCH 1/5] TASK: Render menu prototypes without fluid The prototypes `Neos.Neos:Menu`, `Neos.Neos:DimensionsMenu` and `Neos.Neos:BreadcrumbMenu` are now implemented in pure fusion. The prototype `Neos.Neos:MenuItemListRenderer` is added that is used by those prototypes to render the result as a very basic unordered list. The flag `calculateItemStates` is added to the prototypes `Neos.Neos:MenuItems`, `Neos.Neos:DimensionsMenuItems` and `Neos.Neos:BreadcrumbMenuItems` with the default value being false. Once enabled the itemState of each menuItem is calculated. In addition the following issues are fixed: - DimensionMenuItems did not contain a url - MenuItems did not calculate item states. --- .../AbstractMenuItemsImplementation.php | 32 ++++ .../Classes/Fusion/DimensionMenuItem.php | 25 +++ .../DimensionsMenuItemsImplementation.php | 33 ++-- Neos.Neos/Classes/Fusion/MenuItem.php | 55 +----- Neos.Neos/Classes/Fusion/MenuItemState.php | 19 ++- .../Fusion/MenuItemsImplementation.php | 44 +++-- .../References/NeosFusionReference.rst | 158 ++++-------------- .../Fusion/Prototypes/BreadcrumbMenu.fusion | 34 ++-- .../Prototypes/BreadcrumbMenuItems.fusion | 4 + .../Fusion/Prototypes/DimensionsMenu.fusion | 47 +++--- .../Private/Fusion/Prototypes/Menu.fusion | 48 +----- .../Prototypes/MenuItemListRenderer.fusion | 17 ++ .../Fusion/Prototypes/MenuItems.fusion | 1 + .../FusionObjects/BreadcrumbMenu.html | 18 -- .../FusionObjects/DimensionsMenu.html | 22 --- .../Private/Templates/FusionObjects/Menu.html | 18 -- 16 files changed, 239 insertions(+), 336 deletions(-) create mode 100644 Neos.Neos/Classes/Fusion/DimensionMenuItem.php create mode 100644 Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItemListRenderer.fusion delete mode 100644 Neos.Neos/Resources/Private/Templates/FusionObjects/BreadcrumbMenu.html delete mode 100644 Neos.Neos/Resources/Private/Templates/FusionObjects/DimensionsMenu.html delete mode 100644 Neos.Neos/Resources/Private/Templates/FusionObjects/Menu.html diff --git a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php index a732ddf97d3..29323c4aeb2 100644 --- a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php @@ -60,6 +60,13 @@ abstract class AbstractMenuItemsImplementation extends AbstractFusionObject */ protected $renderHiddenInIndex; + /** + * Internal cache for the calculateItemStates property. + * + * @var boolean + */ + protected $calculateItemStates; + /** * Rootline of all nodes from the current node to the site root node, keys are depth of nodes. * @@ -70,6 +77,20 @@ abstract class AbstractMenuItemsImplementation extends AbstractFusionObject #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; + /** + * Should nodes that have "hiddenInIndex" set still be visible in this menu. + * + * @return boolean + */ + public function isCalculateItemStatesEnabled(): bool + { + if ($this->calculateItemStates === null) { + $this->calculateItemStates = (bool)$this->fusionValue('calculateItemStates'); + } + + return $this->calculateItemStates; + } + /** * Should nodes that have "hiddenInIndex" set still be visible in this menu. * @@ -169,4 +190,15 @@ protected function getCurrentNodeRootline(): array return $this->currentNodeRootline; } + + protected function buildUri(Node $node): string + { + $this->runtime->pushContextArray([ + 'itemNode' => $node, + 'documentNode' => $node, + ]); + $uri = $this->runtime->render($this->path . '/itemUriRenderer'); + $this->runtime->popContext(); + return $uri; + } } diff --git a/Neos.Neos/Classes/Fusion/DimensionMenuItem.php b/Neos.Neos/Classes/Fusion/DimensionMenuItem.php new file mode 100644 index 00000000000..08a400dc5aa --- /dev/null +++ b/Neos.Neos/Classes/Fusion/DimensionMenuItem.php @@ -0,0 +1,25 @@ +|null $targetDimensions + */ + public function __construct( + public readonly ?Node $node, + public readonly ?MenuItemState $state = null, + public readonly ?string $label = null, + public readonly ?array $targetDimensions = null, + public readonly ?string $uri = null + ) { + } +} diff --git a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php index 50871a64ebd..facc2047e57 100644 --- a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php @@ -39,7 +39,7 @@ public function getDimension(): array /** * Builds the array of Menu items for this variant menu - * @return array> + * @return array */ protected function buildItems(): array { @@ -86,12 +86,13 @@ protected function buildItems(): array $metadata = $this->determineMetadata($dimensionSpacePoint, $dimensionMenuItemsImplementationInternals); if ($variant === null || !$this->isNodeHidden($variant)) { - $menuItems[] = [ - 'node' => $variant, - 'state' => $this->calculateItemState($variant), - 'label' => $this->determineLabel($variant, $metadata), - 'targetDimensions' => $metadata - ]; + $menuItems[] = new DimensionMenuItem( + $variant, + $this->isCalculateItemStatesEnabled() ? $this->calculateItemState($variant) : null, + $this->determineLabel($variant, $metadata), + $metadata, + $variant ? $this->buildUri($variant) : null + ); } } } @@ -100,15 +101,15 @@ protected function buildItems(): array if ($contentDimensionIdentifierToLimitTo && $valuesToRestrictTo) { $order = array_flip($valuesToRestrictTo); usort($menuItems, function ( - array $menuItemA, - array $menuItemB + DimensionMenuItem $menuItemA, + DimensionMenuItem $menuItemB ) use ( $order, $contentDimensionIdentifierToLimitTo ) { - return (int)$order[$menuItemA['node']?->subgraphIdentity->dimensionSpacePoint->getCoordinate( + return (int)$order[$menuItemA->node?->subgraphIdentity->dimensionSpacePoint->getCoordinate( $contentDimensionIdentifierToLimitTo - )] <=> (int)$order[$menuItemB['node']?->subgraphIdentity->dimensionSpacePoint->getCoordinate( + )] <=> (int)$order[$menuItemB->node?->subgraphIdentity->dimensionSpacePoint->getCoordinate( $contentDimensionIdentifierToLimitTo )]; }); @@ -218,19 +219,19 @@ protected function determineLabel(?Node $variant = null, array $metadata = []): } } - protected function calculateItemState(?Node $variant = null): string + protected function calculateItemState(?Node $variant = null): MenuItemState { if (is_null($variant)) { - return self::STATE_ABSENT; + return MenuItemState::absent(); } if ($variant === $this->currentNode) { - return self::STATE_CURRENT; + return MenuItemState::current(); } - - return self::STATE_NORMAL; + return MenuItemState::normal(); } + /** * In some cases generalization of the other dimension values is feasible * to find a dimension space point in which a variant can be resolved diff --git a/Neos.Neos/Classes/Fusion/MenuItem.php b/Neos.Neos/Classes/Fusion/MenuItem.php index 86c23f1947d..1ee3387c0e7 100644 --- a/Neos.Neos/Classes/Fusion/MenuItem.php +++ b/Neos.Neos/Classes/Fusion/MenuItem.php @@ -11,58 +11,17 @@ */ final class MenuItem { - protected Node $node; - - protected ?MenuItemState $state; - - protected ?string $label; - - protected int $menuLevel; - - /** - * @var array - */ - protected array $children; - - protected ?string $uri; - /** * @param array $children */ public function __construct( - Node $node, - ?MenuItemState $state = null, - ?string $label = null, - int $menuLevel = 1, - array $children = [], - string $uri = null + public readonly Node $node, + public readonly ?MenuItemState $state = null, + public readonly ?string $label = null, + public readonly int $menuLevel = 1, + public readonly array $children = [], + public readonly ?string $uri = null ) { - $this->node = $node; - $this->state = $state; - $this->label = $label; - $this->menuLevel = $menuLevel; - $this->children = $children; - $this->uri = $uri; - } - - public function getNode(): Node - { - return $this->node; - } - - public function getState(): ?MenuItemState - { - return $this->state; - } - - public function getLabel(): ?string - { - return $this->label; - } - - public function getMenuLevel(): int - { - return $this->menuLevel; } /** @@ -75,7 +34,7 @@ public function getChildren(): array /** * @return array - * @deprecated Use getChildren instead + * @deprecated Use children instead */ public function getSubItems(): array { diff --git a/Neos.Neos/Classes/Fusion/MenuItemState.php b/Neos.Neos/Classes/Fusion/MenuItemState.php index 5bae271d731..a45ec59bbfe 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemState.php +++ b/Neos.Neos/Classes/Fusion/MenuItemState.php @@ -41,9 +41,22 @@ public function __construct(string $state) } - /** - * @return MenuItemState - */ + + public static function active(): MenuItemState + { + return new MenuItemState(self::STATE_ACTIVE); + } + + public static function current(): MenuItemState + { + return new MenuItemState(self::STATE_CURRENT); + } + + public static function absent(): MenuItemState + { + return new MenuItemState(self::STATE_ABSENT); + } + public static function normal(): MenuItemState { return new MenuItemState(self::STATE_NORMAL); diff --git a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php index dfb9a4f51d4..bdadc138e94 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php @@ -15,6 +15,7 @@ namespace Neos\Neos\Fusion; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindAncestorNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; @@ -55,6 +56,11 @@ class MenuItemsImplementation extends AbstractMenuItemsImplementation */ protected $maximumLevels; + /** + * Internal cache for the ancestors of the currentNode. + */ + protected ?Nodes $currentNodeAncestors = null; + /** * Runtime cache for the node type constraints to be applied */ @@ -200,7 +206,7 @@ protected function traverseChildren(Subtree $subtree): MenuItem return new MenuItem( $node, - MenuItemState::normal(), + $this->isCalculateItemStatesEnabled() ? $this->calculateItemState($node) : null, $node->getLabel(), $subtree->level, $children, @@ -232,6 +238,7 @@ protected function findMenuStartingPoint(): ?Node 1369596980 ); } + if ($this->getEntryLevel() === 0) { $entryParentNode = $traversalStartingPoint; } elseif ($this->getEntryLevel() < 0) { @@ -285,7 +292,6 @@ function (Node $traversedNode) use (&$traversedHierarchy, $nodeTypeConstraintsWi $entryParentNode = $traversedHierarchy[$this->getEntryLevel() - 1] ?? null; } - return $entryParentNode; } @@ -312,14 +318,32 @@ protected function traverseUpUntilCondition(Node $node, \Closure $callback): voi } while ($shouldContinueTraversal !== false && $node !== null); } - protected function buildUri(Node $node): string + public function getCurrentNodeAncestors(): Nodes + { + if ($this->currentNodeAncestors instanceof Nodes) { + return $this->currentNodeAncestors; + } + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($this->currentNode); + $this->currentNodeAncestors = $subgraph->findAncestorNodes( + $this->currentNode->nodeAggregateId, + FindAncestorNodesFilter::create( + $this->getNodeTypeConstraints() + ) + ); + return $this->currentNodeAncestors; + } + + protected function calculateItemState(Node $node): MenuItemState { - $this->runtime->pushContextArray([ - 'itemNode' => $node, - 'documentNode' => $node, - ]); - $uri = $this->runtime->render($this->path . '/itemUriRenderer'); - $this->runtime->popContext(); - return $uri; + if ($node->nodeAggregateId->equals($this->currentNode->nodeAggregateId)) { + return MenuItemState::current(); + } + $ancestors = $this->getCurrentNodeAncestors(); + foreach ($ancestors as $ancestor) { + if ($node->nodeAggregateId->equals($ancestor->nodeAggregateId)) { + return MenuItemState::active(); + } + } + return MenuItemState::normal(); } } diff --git a/Neos.Neos/Documentation/References/NeosFusionReference.rst b/Neos.Neos/Documentation/References/NeosFusionReference.rst index af991b249d7..742e8445eef 100644 --- a/Neos.Neos/Documentation/References/NeosFusionReference.rst +++ b/Neos.Neos/Documentation/References/NeosFusionReference.rst @@ -966,73 +966,33 @@ Get argument in controller action:: Neos.Neos:Menu -------------- -Render a menu with items for nodes. Extends :ref:`Neos_Fusion__Template`. +Render a menu with items for nodes. -:templatePath: (string) Override the template path -:entryLevel: (integer) Start the menu at the given depth -:maximumLevels: (integer) Restrict the maximum depth of items in the menu (relative to ``entryLevel``) -:startingPoint: (Node) The parent node of the first menu level (defaults to ``node`` context variable) -:lastLevel: (integer) Restrict the menu depth by node depth (relative to site node) -:filter: (string) Filter items by node type (e.g. ``'!My.Site:News,Neos.Neos:Document'``), defaults to ``'Neos.Neos:Document'`` -:renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` -:itemCollection: (array) Explicitly set the Node items for the menu (alternative to ``startingPoints`` and levels) -:attributes: (:ref:`Neos_Fusion__DataStructure`) Extensible attributes for the whole menu -:normal.attributes: (:ref:`Neos_Fusion__DataStructure`) Attributes for normal state -:active.attributes: (:ref:`Neos_Fusion__DataStructure`) Attributes for active state -:current.attributes: (:ref:`Neos_Fusion__DataStructure`) Attributes for current state - -.. note:: The ``items`` of the ``Menu`` are internally calculated with the prototype :ref:`Neos_Neos__MenuItems` which - you can use directly aswell. - -Menu item properties: -^^^^^^^^^^^^^^^^^^^^^ - -:node: (Node) A node instance (with resolved shortcuts) that should be used to link to the item -:originalNode: (Node) Original node for the item -:state: (string) Menu state of the item: ``'normal'``, ``'current'`` (the current node) or ``'active'`` (ancestor of current node) -:label: (string) Full label of the node -:menuLevel: (integer) Menu level the item is rendered on - -Examples: -^^^^^^^^^ - -Custom menu template: -""""""""""""""""""""" - -:: - - menu = Neos.Neos:Menu { - entryLevel = 1 - maximumLevels = 3 - templatePath = 'resource://My.Site/Private/Templates/FusionObjects/MyMenu.html' - } - -Menu including site node: -""""""""""""""""""""""""" +:attributes: (:ref:`Neos_Fusion__DataStructure`) attributes for the whole menu +:[key]: (mixed) all other fusion properties are passed over to :ref:`Neos_Neos__MenuItems` internally to calculate the `items` -:: +Example:: menu = Neos.Neos:Menu { - itemCollection = ${q(site).add(q(site).children('[instanceof Neos.Neos:Document]')).get()} + attributes.class = 'menu' + maximumLevels = 3 } -Menu with custom starting point: -"""""""""""""""""""""""""""""""" +.. note:: The ``items`` of the ``Menu`` are internally calculated with the prototype :ref:`Neos_Neos__MenuItems` which + you can use directly aswell. -:: - - menu = Neos.Neos:Menu { - entryLevel = 2 - maximumLevels = 1 - startingPoint = ${q(site).children('[uriPathSegment="metamenu"]').get(0)} - } +.. note:: The ``rendering`` of the ``Menu`` is performed with the prototype :ref:`Neos_Neos__MenuItemListRenderer`. + If the rendering does not suit your useCase it we recommended to create your own variants of the menu and renderer prototype. .. _Neos_Neos__BreadcrumbMenu: Neos.Neos:BreadcrumbMenu ------------------------ -Render a breadcrumb (ancestor documents), based on :ref:`Neos_Neos__Menu`. +Render a breadcrumb (ancestor documents). + +:attributes: (:ref:`Neos_Fusion__DataStructure`) attributes for the whole menu +:[key]: (mixed) all other fusion properties are passed over to :ref:`Neos_Neos__BreadcrumbMenuItems` internally Example:: @@ -1041,91 +1001,36 @@ Example:: .. note:: The ``items`` of the ``BreadcrumbMenu`` are internally calculated with the prototype :ref:`Neos_Neos__MenuItems` which you can use directly aswell. +.. note:: The ``rendering`` of the ``BreadcrumbMenu`` is performed with the prototype :ref:`Neos_Neos__MenuItemListRenderer`. + If the rendering does not suit your useCase it we recommended to create your own variants of the menu and renderer prototype. + .. _Neos_Neos__DimensionMenu: .. _Neos_Neos__DimensionsMenu: Neos.Neos:DimensionsMenu ------------------------ -Create links to other node variants (e.g. variants of the current node in other dimensions) by using this Fusion object. +Create links to other node variants (e.g. variants of the current node in other dimensions). -If the ``dimension`` setting is given, the menu will only include items for this dimension, with all other configured -dimension being set to the value(s) of the current node. Without any ``dimension`` being configured, all possible -variants will be included. +:attributes: (:ref:`Neos_Fusion__DataStructure`) attributes for the whole menu +:[key]: (mixed) all other fusion properties are passed over to :ref:`Neos_Neos__DimensionsMenuItems` internally -If no node variant exists for the preset combination, a ``NULL`` node will be included in the item with a state ``absent``. - -:dimension: (optional, string): name of the dimension which this menu should be based on. Example: "language". -:presets: (optional, array): If set, the presets rendered will be taken from this list of preset identifiers -:includeAllPresets: (boolean, default **false**) If TRUE, include all presets, not only allowed combinations -:renderHiddenInIndex: (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" - -In the template for the menu, each ``item`` has the following properties: - -:node: (Node) A node instance (with resolved shortcuts) that should be used to link to the item -:state: (string) Menu state of the item: ``normal``, ``current`` (the current node), ``absent`` -:label: (string) Label of the item (the dimension preset label) -:menuLevel: (integer) Menu level the item is rendered on -:dimensions: (array) Dimension values of the node, indexed by dimension name -:targetDimensions: (array) The target dimensions, indexed by dimension name and values being arrays with ``value``, ``label`` and ``isPinnedDimension`` - -.. note:: The ``DimensionMenu`` is an alias to ``DimensionsMenu``, available for compatibility reasons only. - -.. note:: The ``items`` of the ``DimensionsMenu`` are internally calculated with the prototype :ref:`Neos_Neos__DimensionsMenuItems` which +.. note:: The ``items`` of the ``DimensionsMenu`` are internally calculated with the prototype :ref:`Neos_Neos__DimensionsMenuMenuItems` which you can use directly aswell. -Examples -^^^^^^^^ +.. note:: The ``rendering`` of the ``DimensionsMenu`` is performed with the prototype :ref:`Neos_Neos__MenuItemListRenderer`. + If the rendering does not suit your useCase it we recommended to create your own variants of the menu and renderer prototype. -Minimal Example, outputting a menu with all configured dimension combinations:: +.. _Neos_Neos__MenuItemListRenderer: - variantMenu = Neos.Neos:DimensionsMenu +Neos.Neos:MenuItemListRenderer +------------------------------- -This example will create two menus, one for the 'language' and one for the 'country' dimension:: +A very basic renderer that takes a list of MenuItems and renders the result as unordered list. If item states were calculated +they are applied as classnames to the list items. - languageMenu = Neos.Neos:DimensionsMenu { - dimension = 'language' - } - countryMenu = Neos.Neos:DimensionsMenu { - dimension = 'country' - } - -If you only want to render a subset of the available presets or manually define a specific order for a menu, -you can override the "presets":: - - languageMenu = Neos.Neos:DimensionsMenu { - dimension = 'language' - presets = ${['en_US', 'de_DE']} # no matter how many languages are defined, only these two are displayed. - } - -In some cases, it can be good to ignore the availability of variants when rendering a dimensions menu. Consider a -situation with two independent menus for country and language, where the following variants of a node exist -(language / country): - -- english / Germany -- german / Germany -- english / UK - -If the user selects UK, only english will be linked in the language selector. German is only available again, if the -user switches back to Germany first. This can be changed by setting the ``includeAllPresets`` option:: - - languageMenu = Neos.Neos:DimensionsMenu { - dimension = 'language' - includeAllPresets = true - } - -Now the language menu will try to find nodes for all languages, if needed the menu items will point to a different -country than currently selected. The menu tries to find a node to link to by using the current preset for the language -(in this example) and the default presets for any other dimensions. So if fallback rules are in place and a node can be -found, it is used. - -.. note:: The ``item.targetDimensions`` will contain the "intended" dimensions, so that information can be used to - inform the user about the potentially unexpected change of dimensions when following such a link. - -Only if the current node is not available at all (even after considering default presets with their fallback rules), -no node be assigned (so no link will be created and the items will have the ``absent`` state.) - -.. _Neos_Neos__MenuItems: +:items: (array): The MenuItems as generated by :ref:`Neos_Neos__MenuItems`, :ref:`Neos_Neos__DimensionsMenuItems`, :ref:`Neos_Neos__BreadcrumbMenuItems` +:attributes: (optional, array): The attributes to apply on the outer list Neos.Neos:MenuItems ------------------- @@ -1140,6 +1045,7 @@ Create a list of menu-items items for nodes. :renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` :itemCollection: (array) Explicitly set the Node items for the menu (alternative to ``startingPoints`` and levels) :itemUriRenderer: (:ref:`Neos_Neos__NodeUri`) prototype to use for rendering the URI of each item +:calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` MenuItems item properties: ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1220,6 +1126,8 @@ If no node variant exists for the preset combination, a ``NULL`` node will be in :presets: (optional, array): If set, the presets rendered will be taken from this list of preset identifiers :includeAllPresets: (boolean, default **false**) If TRUE, include all presets, not only allowed combinations :renderHiddenInIndex: (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" +:itemUriRenderer: (:ref:`Neos_Neos__NodeUri`) prototype to use for rendering the URI of each item +:calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` Each ``item`` has the following properties: diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion index 2e7903596d6..0a9b9fcc365 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion @@ -1,17 +1,29 @@ # Neos.Neos:BreadcrumbMenu provides a breadcrumb navigation based on menu items. # -prototype(Neos.Neos:BreadcrumbMenu) < prototype(Neos.Neos:Menu) { - templatePath = 'resource://Neos.Neos/Private/Templates/FusionObjects/BreadcrumbMenu.html' +prototype(Neos.Neos:BreadcrumbMenu) < prototype(Neos.Fusion:Component) { + attributes = Neos.Fusion:DataStructure - itemCollection = ${q(documentNode).parents('[instanceof Neos.Neos:Document]').get()} - // Show always the current node, event when it is hidden in index - items.@process.addCurrent = Neos.Fusion:Value { - currentItem = Neos.Neos:MenuItems { - renderHiddenInIndex = true - itemCollection = ${[documentNode]} - } - value = ${Array.concat(this.currentItem, value)} + @private { + items = Neos.Neos:BreadcrumbMenuItems { + @ignoreProperties = ${['attributes']} + @apply.props = ${props} + } } - attributes.class = 'breadcrumb' + renderer = Neos.Neos:MenuItemListRenderer { + items = ${private.items} + attributes = ${props.attributes} + } + + @exceptionHandler = 'Neos\\Fusion\\Core\\ExceptionHandlers\\ContextDependentHandler' + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(documentNode)} + } + entryTags { + 1 = ${Neos.Caching.nodeTypeTag('Neos.Neos:Document', documentNode)} + } + } } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion index e06d3b8790e..3e66c9b455e 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion @@ -1,10 +1,14 @@ prototype(Neos.Neos:BreadcrumbMenuItems) < prototype(Neos.Neos:MenuItems) { itemCollection = ${q(documentNode).parents('[instanceof Neos.Neos:Document]').get()} + maximumLevels = 0 + @process { // Show always the current node, event when it is hidden in index addCurrent = Neos.Fusion:Value { currentItem = Neos.Neos:MenuItems { + calculateItemStates = ${props.calculateItemStates} renderHiddenInIndex = true + maximumLevels = 0 itemCollection = ${[documentNode]} } value = ${Array.concat(this.currentItem, value)} diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion index f5ded703887..fccffdb3e06 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion @@ -1,34 +1,29 @@ # Neos.Neos:DimensionsMenu provides dimension (e.g. language) menu rendering -prototype(Neos.Neos:DimensionsMenu) < prototype(Neos.Neos:Menu) { - templatePath = 'resource://Neos.Neos/Private/Templates/FusionObjects/DimensionsMenu.html' - - # the "absent" state is assigned to items for dimension (combinations) for which no node variant exists - absent.attributes = Neos.Fusion:DataStructure { - class = 'normal' +prototype(Neos.Neos:DimensionsMenu) < prototype(Neos.Fusion:Component) { + attributes = Neos.Fusion:DataStructure + + @private { + items = Neos.Neos:DimensionsMenuItems { + @ignoreProperties = ${['attributes']} + @apply.props = ${props} + } } - # if documents which are hidden in index should be rendered or not - renderHiddenInIndex = true - - # name of the dimension to use (optional) - dimension = null - - # list of presets, if the default order should be overridden, only used with "dimension" set - presets = null - - # if true, items for all presets will be included, ignoring dimension constraints - includeAllPresets = false - - @context { - dimension = ${this.dimension} - presets = ${this.presets} - includeAllPresets = ${this.includeAllPresets} + renderer = Neos.Neos:MenuItemListRenderer { + items = ${private.items} + attributes = ${props.attributes} } - items = Neos.Neos:DimensionsMenuItems { - includeAllPresets = ${includeAllPresets} - dimension = ${dimension} - presets = ${presets} + @exceptionHandler = 'Neos\\Fusion\\Core\\ExceptionHandlers\\ContextDependentHandler' + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(documentNode)} + } + entryTags { + 1 = ${Neos.Caching.nodeTypeTag('Neos.Neos:Document', documentNode)} + } } } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion index 570b9af36d0..aca16e43107 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion @@ -1,48 +1,18 @@ # Neos.Neos:Menu provides basic menu rendering # -prototype(Neos.Neos:Menu) < prototype(Neos.Fusion:Template) { - templatePath = 'resource://Neos.Neos/Private/Templates/FusionObjects/Menu.html' - - entryLevel = ${this.startingPoint ? 0 : 1} - maximumLevels = 2 - startingPoint = null - lastLevel = null - filter = 'Neos.Neos:Document' - renderHiddenInIndex = false - itemCollection = null - - node = ${node} - +prototype(Neos.Neos:Menu) < prototype(Neos.Fusion:Component) { attributes = Neos.Fusion:DataStructure - active.attributes = Neos.Fusion:DataStructure { - class = 'active' - } - current.attributes = Neos.Fusion:DataStructure { - class = 'current' - } - normal.attributes = Neos.Fusion:DataStructure { - class = 'normal' - } - - @context { - entryLevel = ${this.entryLevel} - maximumLevels = ${this.maximumLevels} - startingPoint = ${this.startingPoint} - lastLevel = ${this.lastLevel} - filter = ${this.filter} - renderHiddenInIndex = ${this.renderHiddenInIndex} - itemCollection = ${this.itemCollection} + @private { + items = Neos.Neos:MenuItems { + @ignoreProperties = ${['attributes']} + @apply.props = ${props} + } } - items = Neos.Neos:MenuItems { - entryLevel = ${entryLevel} - maximumLevels = ${maximumLevels} - startingPoint = ${startingPoint} - lastLevel = ${lastLevel} - filter = ${filter} - renderHiddenInIndex = ${renderHiddenInIndex} - itemCollection = ${itemCollection} + renderer = Neos.Neos:MenuItemListRenderer { + items = ${private.items} + attributes = ${props.attributes} } @exceptionHandler = 'Neos\\Fusion\\Core\\ExceptionHandlers\\ContextDependentHandler' diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItemListRenderer.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItemListRenderer.fusion new file mode 100644 index 00000000000..54203902617 --- /dev/null +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItemListRenderer.fusion @@ -0,0 +1,17 @@ +# Neos.Neos:MenuItemListRenderer provides basic menu rendering +# +prototype(Neos.Neos:MenuItemListRenderer) < prototype(Neos.Fusion:Component) { + items = null + attributes = Neos.Fusion:DataStructure + + renderer = afx` +
    + +
  • + {item.label} + +
  • +
    +
+ ` +} diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion index 01dbeb06484..d2ef5d4b7f1 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion @@ -9,6 +9,7 @@ prototype(Neos.Neos:MenuItems) { filter = 'Neos.Neos:Document' renderHiddenInIndex = false itemCollection = null + calculateItemStates = false // This property is used internally by `MenuItemsImplementation` to render each items uri. // It can be modified to change behaviour for all rendered uris. diff --git a/Neos.Neos/Resources/Private/Templates/FusionObjects/BreadcrumbMenu.html b/Neos.Neos/Resources/Private/Templates/FusionObjects/BreadcrumbMenu.html deleted file mode 100644 index f96b3e7c63c..00000000000 --- a/Neos.Neos/Resources/Private/Templates/FusionObjects/BreadcrumbMenu.html +++ /dev/null @@ -1,18 +0,0 @@ -{namespace neos=Neos\Neos\ViewHelpers} -{namespace ts=Neos\Fusion\ViewHelpers} - - f:format.raw()}> - - f:format.raw()}> - - - {item.label} - - - {item.label} - - - - - - diff --git a/Neos.Neos/Resources/Private/Templates/FusionObjects/DimensionsMenu.html b/Neos.Neos/Resources/Private/Templates/FusionObjects/DimensionsMenu.html deleted file mode 100644 index 4b646319db0..00000000000 --- a/Neos.Neos/Resources/Private/Templates/FusionObjects/DimensionsMenu.html +++ /dev/null @@ -1,22 +0,0 @@ -{namespace neos=Neos\Neos\ViewHelpers} -{namespace ts=Neos\Fusion\ViewHelpers} - f:format.raw()}> - - f:format.raw()}> - - - - - - - - - - - - - - - - {item.label} - diff --git a/Neos.Neos/Resources/Private/Templates/FusionObjects/Menu.html b/Neos.Neos/Resources/Private/Templates/FusionObjects/Menu.html deleted file mode 100644 index 27f33b88ebe..00000000000 --- a/Neos.Neos/Resources/Private/Templates/FusionObjects/Menu.html +++ /dev/null @@ -1,18 +0,0 @@ -{namespace neos=Neos\Neos\ViewHelpers} -{namespace ts=Neos\Fusion\ViewHelpers} - f:format.raw()}> - - - - - - f:format.raw()}> - {item.label} - -
    - -
-
- -
-
From e78011f49cbcf16e14707247c6521dc91bf18321 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 20 Oct 2023 16:19:20 +0200 Subject: [PATCH 2/5] TASK: Store NodeAggregateIds of current node rootline instead of full nodes This is more memory efficient and we only compare the AggregateIds anyways In addition the signature of `NodeAggregateIds::fromNodes(array $nodes)` is changed to `NodeAggregateIds::fromNodes(Nodes $nodes)` and the method `NodeAggregateIds::contain(NodeAggregateId $nodeAggregateId)` is added. There were no users of this to be found so this should be safe. --- .../SharedModel/Node/NodeAggregateIds.php | 13 ++++++---- .../Fusion/MenuItemsImplementation.php | 24 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAggregateIds.php b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAggregateIds.php index 66a8016be69..ca554821d62 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAggregateIds.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAggregateIds.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core\SharedModel\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; /** * An immutable collection of NodeAggregateIds, indexed by their value @@ -74,13 +75,10 @@ public static function fromJsonString(string $jsonString): self return self::fromArray(\json_decode($jsonString, true)); } - /** - * @param Node[] $nodes - */ - public static function fromNodes(array $nodes): self + public static function fromNodes(Nodes $nodes): self { return self::fromArray( - array_map(fn(Node $node) => $node->nodeAggregateId, $nodes) + array_map(fn(Node $node) => $node->nodeAggregateId, iterator_to_array($nodes)) ); } @@ -92,6 +90,11 @@ public function merge(self $other): self )); } + public function contain(NodeAggregateId $nodeAggregateId): bool + { + return array_key_exists($nodeAggregateId->value, $this->nodeAggregateIds); + } + /** * @return array */ diff --git a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php index bdadc138e94..04c693b7799 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php @@ -22,6 +22,8 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraintsWithSubNodeTypes; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\Fusion\Exception as FusionException; use Neos\Neos\Domain\Service\NodeTypeNameFactory; @@ -57,9 +59,9 @@ class MenuItemsImplementation extends AbstractMenuItemsImplementation protected $maximumLevels; /** - * Internal cache for the ancestors of the currentNode. + * Internal cache for the ancestors aggregate ids of the currentNode. */ - protected ?Nodes $currentNodeAncestors = null; + protected ?NodeAggregateIds $currentNodeAncestorAggregateIds = null; /** * Runtime cache for the node type constraints to be applied @@ -318,19 +320,20 @@ protected function traverseUpUntilCondition(Node $node, \Closure $callback): voi } while ($shouldContinueTraversal !== false && $node !== null); } - public function getCurrentNodeAncestors(): Nodes + public function getCurrentNodeAncestorAggregateIds(): NodeAggregateIds { - if ($this->currentNodeAncestors instanceof Nodes) { - return $this->currentNodeAncestors; + if ($this->currentNodeAncestorAggregateIds instanceof NodeAggregateIds) { + return $this->currentNodeAncestorAggregateIds; } $subgraph = $this->contentRepositoryRegistry->subgraphForNode($this->currentNode); - $this->currentNodeAncestors = $subgraph->findAncestorNodes( + $currentNodeAncestors = $subgraph->findAncestorNodes( $this->currentNode->nodeAggregateId, FindAncestorNodesFilter::create( $this->getNodeTypeConstraints() ) ); - return $this->currentNodeAncestors; + $this->currentNodeAncestorAggregateIds = NodeAggregateIds::fromNodes($currentNodeAncestors); + return $this->currentNodeAncestorAggregateIds; } protected function calculateItemState(Node $node): MenuItemState @@ -338,11 +341,8 @@ protected function calculateItemState(Node $node): MenuItemState if ($node->nodeAggregateId->equals($this->currentNode->nodeAggregateId)) { return MenuItemState::current(); } - $ancestors = $this->getCurrentNodeAncestors(); - foreach ($ancestors as $ancestor) { - if ($node->nodeAggregateId->equals($ancestor->nodeAggregateId)) { - return MenuItemState::active(); - } + if ($this->getCurrentNodeAncestorAggregateIds()->contain($node->nodeAggregateId)) { + return MenuItemState::active(); } return MenuItemState::normal(); } From e603c21698223950240b19888e879c6e9418ec93 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 20 Oct 2023 16:22:20 +0200 Subject: [PATCH 3/5] Update Neos.Neos/Classes/Fusion/DimensionMenuItem.php Co-authored-by: Bastian Waidelich --- Neos.Neos/Classes/Fusion/DimensionMenuItem.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/DimensionMenuItem.php b/Neos.Neos/Classes/Fusion/DimensionMenuItem.php index 08a400dc5aa..cb7ff321d1c 100644 --- a/Neos.Neos/Classes/Fusion/DimensionMenuItem.php +++ b/Neos.Neos/Classes/Fusion/DimensionMenuItem.php @@ -7,19 +7,20 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; /** - * A menu item + * A menu item for dimension menus + * Compared to the default {@see MenuItem} it has no `menuLevel` property, but one for the `targetDimensions` */ -final class DimensionMenuItem +final readonly class DimensionMenuItem { /** * @param array|null $targetDimensions */ public function __construct( - public readonly ?Node $node, - public readonly ?MenuItemState $state = null, - public readonly ?string $label = null, - public readonly ?array $targetDimensions = null, - public readonly ?string $uri = null + public ?Node $node, + public ?MenuItemState $state = null, + public ?string $label = null, + public ?array $targetDimensions = null, + public ?string $uri = null ) { } } From 6134527f794db548ed0db40f9692c27b59732e5e Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 20 Oct 2023 16:26:47 +0200 Subject: [PATCH 4/5] Update Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php Co-authored-by: Bastian Waidelich --- Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php index 29323c4aeb2..d37385261f4 100644 --- a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php @@ -78,9 +78,8 @@ abstract class AbstractMenuItemsImplementation extends AbstractFusionObject protected ContentRepositoryRegistry $contentRepositoryRegistry; /** - * Should nodes that have "hiddenInIndex" set still be visible in this menu. - * - * @return boolean + * Whether the active/current state of menu items is calculated on the server side. + * This has an effect on performance and caching */ public function isCalculateItemStatesEnabled(): bool { From 300f84f0755eddff29464f29273cf3531111b559 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 20 Oct 2023 17:04:23 +0200 Subject: [PATCH 5/5] TASK: Make MenuItem and DimensionMenuItemReadonly and convert MenuItemState to a BackedEnum In addition the RenderAttributesTrait was adjusted to handle backed enums like stringables. --- .../Classes/Service/RenderAttributesTrait.php | 6 ++ .../Tests/Unit/Service/HtmlAugmenterTest.php | 32 ++++++++- .../DimensionsMenuItemsImplementation.php | 6 +- Neos.Neos/Classes/Fusion/MenuItem.php | 14 ++-- Neos.Neos/Classes/Fusion/MenuItemState.php | 66 ++----------------- .../Fusion/MenuItemsImplementation.php | 6 +- 6 files changed, 55 insertions(+), 75 deletions(-) diff --git a/Neos.Fusion/Classes/Service/RenderAttributesTrait.php b/Neos.Fusion/Classes/Service/RenderAttributesTrait.php index 9063bb17ed5..ca6af143c16 100644 --- a/Neos.Fusion/Classes/Service/RenderAttributesTrait.php +++ b/Neos.Fusion/Classes/Service/RenderAttributesTrait.php @@ -42,6 +42,8 @@ protected function renderAttributes(iterable $attributes, bool $allowEmpty = tru foreach ($attributeValue as $attributeValuePart) { if ($attributeValuePart instanceof \Stringable) { $attributeValuePart = $attributeValuePart->__toString(); + } elseif ($attributeValuePart instanceof \BackedEnum) { + $attributeValuePart = $attributeValuePart->value; } $joinedAttributeValue .= match (gettype($attributeValuePart)) { 'boolean', 'NULL' => '', @@ -50,6 +52,10 @@ protected function renderAttributes(iterable $attributes, bool $allowEmpty = tru }; } $attributeValue = trim($joinedAttributeValue); + } elseif ($attributeValue instanceof \Stringable) { + $attributeValue = $attributeValue->__toString(); + } elseif ($attributeValue instanceof \BackedEnum) { + $attributeValue = $attributeValue->value; } $encodedAttributeName = htmlspecialchars((string)$attributeName, ENT_COMPAT, 'UTF-8', false); if ($attributeValue === true || $attributeValue === '') { diff --git a/Neos.Fusion/Tests/Unit/Service/HtmlAugmenterTest.php b/Neos.Fusion/Tests/Unit/Service/HtmlAugmenterTest.php index 820b2d7afe5..c2befa76c02 100644 --- a/Neos.Fusion/Tests/Unit/Service/HtmlAugmenterTest.php +++ b/Neos.Fusion/Tests/Unit/Service/HtmlAugmenterTest.php @@ -47,9 +47,14 @@ public function __toString() { return "casted value"; } } + enum BackedStringEnum: string { + case Example = "enum value"; + } '); + /** @noinspection PhpUndefinedClassInspection */ $mockObject = new \ClassWithToStringMethod(); + $mockEnum = \BackedStringEnum::Example; return [ // object values with __toString method @@ -61,7 +66,15 @@ public function __toString() { 'allowEmpty' => true, 'expectedResult' => '
' ], - + // object values with BackendEnum value + [ + 'html' => '', + 'attributes' => ['enum' => $mockEnum], + 'fallbackTagName' => null, + 'exclusiveAttributes' => null, + 'allowEmpty' => true, + 'expectedResult' => '
' + ], // empty source [ 'html' => '', @@ -344,6 +357,23 @@ public function __toString() { 'allowEmpty' => false, 'expectedResult' => '

Stringable attribute

', ], + // Adding of Enum attributes + [ + 'html' => '

Enum attribute

', + 'attributes' => ['data-enum' => $mockEnum], + 'fallbackTagName' => null, + 'exclusiveAttributes' => null, + 'allowEmpty' => true, + 'expectedResult' => '

Enum attribute

', + ], + [ + 'html' => '

Enum attribute

', + 'attributes' => ['data-enum' => $mockEnum], + 'fallbackTagName' => null, + 'exclusiveAttributes' => null, + 'allowEmpty' => false, + 'expectedResult' => '

Enum attribute

', + ], // Adding of array attributes [ 'html' => '

Array attribute

', diff --git a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php index facc2047e57..6877fe98c6d 100644 --- a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php @@ -222,13 +222,13 @@ protected function determineLabel(?Node $variant = null, array $metadata = []): protected function calculateItemState(?Node $variant = null): MenuItemState { if (is_null($variant)) { - return MenuItemState::absent(); + return MenuItemState::ABSENT; } if ($variant === $this->currentNode) { - return MenuItemState::current(); + return MenuItemState::CURRENT; } - return MenuItemState::normal(); + return MenuItemState::NORMAL; } diff --git a/Neos.Neos/Classes/Fusion/MenuItem.php b/Neos.Neos/Classes/Fusion/MenuItem.php index 1ee3387c0e7..59de848a70c 100644 --- a/Neos.Neos/Classes/Fusion/MenuItem.php +++ b/Neos.Neos/Classes/Fusion/MenuItem.php @@ -9,18 +9,18 @@ /** * A menu item */ -final class MenuItem +final readonly class MenuItem { /** * @param array $children */ public function __construct( - public readonly Node $node, - public readonly ?MenuItemState $state = null, - public readonly ?string $label = null, - public readonly int $menuLevel = 1, - public readonly array $children = [], - public readonly ?string $uri = null + public Node $node, + public ?MenuItemState $state = null, + public ?string $label = null, + public int $menuLevel = 1, + public array $children = [], + public ?string $uri = null ) { } diff --git a/Neos.Neos/Classes/Fusion/MenuItemState.php b/Neos.Neos/Classes/Fusion/MenuItemState.php index a45ec59bbfe..2430cc3a75f 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemState.php +++ b/Neos.Neos/Classes/Fusion/MenuItemState.php @@ -7,66 +7,10 @@ /** * The menu item state value object */ -final class MenuItemState +enum MenuItemState: string { - public const STATE_NORMAL = 'normal'; - public const STATE_CURRENT = 'current'; - public const STATE_ACTIVE = 'active'; - public const STATE_ABSENT = 'absent'; - - /** - * @var string - */ - protected $state; - - /** - * @param string $state - * @throws Exception\InvalidMenuItemStateException - */ - public function __construct(string $state) - { - if ( - $state !== self::STATE_NORMAL - && $state !== self::STATE_CURRENT - && $state !== self::STATE_ACTIVE - && $state !== self::STATE_ABSENT - ) { - throw new Exception\InvalidMenuItemStateException( - '"' . $state . '" is no valid menu item state', - 1519668881 - ); - } - - $this->state = $state; - } - - - - public static function active(): MenuItemState - { - return new MenuItemState(self::STATE_ACTIVE); - } - - public static function current(): MenuItemState - { - return new MenuItemState(self::STATE_CURRENT); - } - - public static function absent(): MenuItemState - { - return new MenuItemState(self::STATE_ABSENT); - } - - public static function normal(): MenuItemState - { - return new MenuItemState(self::STATE_NORMAL); - } - - /** - * @return string - */ - public function __toString(): string - { - return $this->state; - } + case NORMAL = 'normal'; + case CURRENT = 'current'; + case ACTIVE = 'active'; + case ABSENT = 'absent'; } diff --git a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php index 04c693b7799..f88a11120b7 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php @@ -339,11 +339,11 @@ public function getCurrentNodeAncestorAggregateIds(): NodeAggregateIds protected function calculateItemState(Node $node): MenuItemState { if ($node->nodeAggregateId->equals($this->currentNode->nodeAggregateId)) { - return MenuItemState::current(); + return MenuItemState::CURRENT; } if ($this->getCurrentNodeAncestorAggregateIds()->contain($node->nodeAggregateId)) { - return MenuItemState::active(); + return MenuItemState::ACTIVE; } - return MenuItemState::normal(); + return MenuItemState::NORMAL; } }