Skip to content

Commit 8ee9830

Browse files
committed
Remove active category in the cache key
- Return data about product and category suffixes from ViewModel to define on JS if url is related to product or category;
1 parent 1513c10 commit 8ee9830

File tree

4 files changed

+206
-54
lines changed

4 files changed

+206
-54
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\ViewModel;
9+
10+
use Magento\Framework\App\Config\ScopeConfigInterface;
11+
use Magento\Framework\DataObject;
12+
use Magento\Framework\Serialize\Serializer\Json;
13+
use Magento\Framework\Serialize\Serializer\JsonHexTag;
14+
use Magento\Framework\View\Element\Block\ArgumentInterface;
15+
use Magento\Framework\Escaper;
16+
use Magento\Store\Model\ScopeInterface;
17+
18+
/**
19+
* Navigation menu view model.
20+
*/
21+
class TopMenu extends DataObject implements ArgumentInterface
22+
{
23+
private const XML_PATH_PRODUCT_URL_SUFFIX = 'catalog/seo/product_url_suffix';
24+
private const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix';
25+
private const XML_PATH_PRODUCT_USE_CATEGORIES = 'catalog/seo/product_use_categories';
26+
27+
/**
28+
* @var ScopeConfigInterface
29+
*/
30+
private $scopeConfig;
31+
32+
/**
33+
* @var Escaper
34+
*/
35+
private $escaper;
36+
37+
/**
38+
* @var Json
39+
*/
40+
private $jsonSerializer;
41+
42+
/**
43+
* @param ScopeConfigInterface $scopeConfig
44+
* @param Escaper $escaper
45+
* @param JsonHexTag $jsonSerializer
46+
*/
47+
public function __construct(
48+
ScopeConfigInterface $scopeConfig,
49+
Escaper $escaper,
50+
JsonHexTag $jsonSerializer
51+
) {
52+
parent::__construct();
53+
54+
$this->scopeConfig = $scopeConfig;
55+
$this->escaper = $escaper;
56+
$this->jsonSerializer = $jsonSerializer;
57+
}
58+
59+
/**
60+
* Returns product URL suffix.
61+
*
62+
* @return mixed
63+
*/
64+
public function getProductUrlSuffix()
65+
{
66+
return $this->scopeConfig->getValue(
67+
self::XML_PATH_PRODUCT_URL_SUFFIX,
68+
ScopeInterface::SCOPE_STORE
69+
);
70+
}
71+
72+
/**
73+
* Returns category URL suffix.
74+
*
75+
* @return mixed
76+
*/
77+
public function getCategoryUrlSuffix()
78+
{
79+
return $this->scopeConfig->getValue(
80+
self::XML_PATH_CATEGORY_URL_SUFFIX,
81+
ScopeInterface::SCOPE_STORE
82+
);
83+
}
84+
85+
/**
86+
* Checks if categories path is used for product URLs.
87+
*
88+
* @return bool
89+
*/
90+
public function isCategoryUsedInProductUrl(): bool
91+
{
92+
return $this->scopeConfig->isSetFlag(
93+
self::XML_PATH_PRODUCT_USE_CATEGORIES,
94+
ScopeInterface::SCOPE_STORE
95+
);
96+
}
97+
98+
/**
99+
* Public getter for the JSON serializer.
100+
*
101+
* @return Json
102+
*/
103+
public function getJsonSerializer()
104+
{
105+
return $this->jsonSerializer;
106+
}
107+
108+
/**
109+
* Returns menu json with html escaped names
110+
*
111+
* @return string
112+
*/
113+
public function getJsonConfigurationHtmlEscaped(): string
114+
{
115+
return $this->jsonSerializer->serialize(
116+
[
117+
'menu' => [
118+
'productUrlSuffix' => $this->escaper->escapeHtml($this->getProductUrlSuffix()),
119+
'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()),
120+
'useCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl()
121+
]
122+
]
123+
);
124+
}
125+
}

app/code/Magento/Theme/view/frontend/layout/default.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@
7373
<arguments>
7474
<argument name="title" translate="true" xsi:type="string">Menu</argument>
7575
</arguments>
76-
<block class="Magento\Theme\Block\Html\Topmenu" name="catalog.topnav" template="Magento_Theme::html/topmenu.phtml" ttl="3600" before="-"/>
76+
<block class="Magento\Theme\Block\Html\Topmenu" name="catalog.topnav" template="Magento_Theme::html/topmenu.phtml" ttl="3600" before="-">
77+
<arguments>
78+
<argument name="viewModel" xsi:type="object">Magento\Catalog\ViewModel\TopMenu</argument>
79+
</arguments>
80+
</block>
7781
</block>
7882
<block class="Magento\Framework\View\Element\Text" name="store.links" group="navigation-sections">
7983
<arguments>

app/code/Magento/Theme/view/frontend/templates/html/topmenu.phtml

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@
44
* See COPYING.txt for license details.
55
*/
66

7-
/**
8-
* Top menu for store
9-
*
10-
* @var $block \Magento\Theme\Block\Html\Topmenu
11-
*/
12-
7+
/** @var $block \Magento\Theme\Block\Html\Topmenu */
8+
/** @var $viewModel \Magento\Catalog\ViewModel\TopMenu */
9+
$viewModel = $block->getData('viewModel');
1310
$columnsLimit = $block->getColumnsLimit() ?: 0;
14-
$_menuHtml = $block->getHtml('level-top', 'submenu', $columnsLimit)
11+
$menuHtml = $block->getHtml('level-top', 'submenu', $columnsLimit);
12+
$jsonSerializer = $viewModel->getJsonSerializer();
13+
14+
$widget = $jsonSerializer->unserialize($viewModel->getJsonConfigurationHtmlEscaped());
15+
$widgetOptions = [
16+
'menu' => array_merge(
17+
$widget['menu'] ?? [],
18+
[
19+
'responsive' => true,
20+
'expanded' => true,
21+
'position' => ['my' => 'left top', 'at' => 'left bottom']
22+
])
23+
];
24+
$widgetOptionsJson = $jsonSerializer->serialize($widgetOptions);
1525
?>
1626

1727
<nav class="navigation" data-action="navigation">
18-
<ul data-mage-init='{"menu":{"responsive":true, "expanded":true, "position":{"my":"left top","at":"left bottom"}}}'>
19-
<?= /* @noEscape */ $_menuHtml?>
28+
<ul data-mage-init='<?= /* @noEscape */ $widgetOptionsJson ?>'>
29+
<?= /* @noEscape */ $menuHtml ?>
2030
<?= $block->getChildHtml() ?>
2131
</ul>
2232
</nav>

lib/web/mage/menu.js

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ define([
1616
*/
1717
$.widget('mage.menu', $.ui.menu, {
1818
options: {
19+
productUrlSuffix: '',
20+
categoryUrlSuffix: '',
21+
useCategoryPathInUrl: false,
1922
responsive: false,
2023
expanded: false,
2124
showDelay: 42,
@@ -122,7 +125,7 @@ define([
122125
var currentUrl = window.location.href.split('?')[0];
123126

124127
if (!this._setActiveMenuForCategory(currentUrl)) {
125-
this._setActiveMenuForProduct();
128+
this._setActiveMenuForProduct(currentUrl);
126129
}
127130
},
128131

@@ -135,9 +138,6 @@ define([
135138
* @private
136139
*/
137140
_setActiveMenuForCategory: function (url) {
138-
// Clear active state from all categories
139-
this._clearActiveState();
140-
141141
var activeCategoryLink = this.element.find('a[href="' + url + '"]'),
142142
classes,
143143
classNav;
@@ -159,19 +159,6 @@ define([
159159
return true;
160160
},
161161

162-
/**
163-
* Clears the active state from all menu items within the navigation element.
164-
* It removes 'active' and 'has-active' classes from all list items (li elements),
165-
* which are used to indicate the currently selected or parent of a selected item.
166-
* This method is typically used to reset the menu's state before applying new active states.
167-
*
168-
* @return void
169-
* @private
170-
*/
171-
_clearActiveState: function () {
172-
this.element.find('li').removeClass('active has-active');
173-
},
174-
175162
/**
176163
* Sets 'has-active' CSS class to all parent categories which have part of provided class in childClassName
177164
*
@@ -198,46 +185,72 @@ define([
198185
},
199186

200187
/**
201-
* Activates the category in the menu corresponding to the current product page.
202-
* It resolves the category URL based on the referrer (if it's from the same domain),
203-
* and then sets this category as active in the menu.
204-
* If no suitable referrer URL is found, no category is set as active.
188+
* Sets the active category in the menu based on the current product page or referrer.
189+
* It checks if the current URL or the referrer URL ends with a category URL extension.
190+
* If a match is found, it sets the corresponding category as active in the menu.
191+
* If no suitable URL is found, it clears the active state from the menu.
205192
*
193+
* @param {String} currentUrl - current page URL without parameters
206194
* @return void
207195
* @private
208196
*/
209-
_setActiveMenuForProduct: function () {
210-
var categoryUrl = this._resolveCategoryUrl();
197+
_setActiveMenuForProduct: function (currentUrl) {
198+
var categoryUrlExtension = this.options.categoryUrlSuffix || '.html',
199+
possibleCategoryUrl,
200+
firstCategoryUrl = this.element.find('> li a').attr('href');
201+
202+
if (firstCategoryUrl) {
203+
var referrer = document.referrer;
204+
205+
// Check if the referrer is from the same domain
206+
if (referrer && new URL(referrer).hostname === window.location.hostname) {
207+
// Remove any query parameters from the referrer URL
208+
var queryParamIndex = referrer.indexOf('?');
209+
if (queryParamIndex > 0) {
210+
referrer = referrer.substring(0, queryParamIndex);
211+
}
212+
}
211213

212-
if (categoryUrl) {
213-
this._setActiveMenuForCategory(categoryUrl);
214+
var isCategoryOrProductPage = this._isCategoryOrProductPage(currentUrl);
215+
216+
if (referrer && referrer.endsWith(categoryUrlExtension) && isCategoryOrProductPage) {
217+
possibleCategoryUrl = referrer;
218+
} else if (isCategoryOrProductPage) {
219+
possibleCategoryUrl = currentUrl.substring(0, currentUrl.lastIndexOf('/')) + categoryUrlExtension;
220+
} else {
221+
this._clearActiveState();
222+
return;
223+
}
224+
225+
this._setActiveMenuForCategory(possibleCategoryUrl);
214226
}
215227
},
216228

217229
/**
218-
* Resolves the category URL based on the referrer URL.
219-
* Checks if the referrer URL belongs to the same domain and is a likely category page.
220-
* If so, returns the referrer URL after stripping any query parameters.
221-
* Returns null if no suitable referrer is found or if it cannot be determined to be a category page.
230+
* Determines whether the given URL is likely a category or product page.
231+
* It checks if the URL ends with the predefined product or category URL suffix.
222232
*
223-
* @return {String|null} The URL of the category, or null if it cannot be determined.
233+
* @param {String} url - The URL to check.
234+
* @return {Boolean} - Returns true if the URL ends with a product or category URL suffix, false otherwise.
224235
* @private
225236
*/
226-
_resolveCategoryUrl: function () {
227-
var categoryUrl = document.referrer;
228-
229-
// Ensure the referrer is from the same domain
230-
if (categoryUrl && new URL(categoryUrl).hostname === window.location.hostname) {
231-
// Remove any query parameters from the referrer URL
232-
var queryParamIndex = categoryUrl.indexOf('?');
233-
if (queryParamIndex > 0) {
234-
categoryUrl = categoryUrl.substring(0, queryParamIndex);
235-
}
236-
return categoryUrl;
237-
}
237+
_isCategoryOrProductPage: function (url) {
238+
var productSuffix = this.options.productUrlSuffix || '.html';
239+
var categorySuffix = this.options.categoryUrlSuffix || '.html';
238240

239-
// Fallback or default behavior if no suitable referrer is found
240-
return null;
241+
return url.endsWith(productSuffix) || url.endsWith(categorySuffix);
242+
},
243+
244+
/**
245+
* Clears the active state from all menu items within the navigation element.
246+
* It removes 'active' and 'has-active' classes from all list items (li elements),
247+
* which are used to indicate the currently selected or parent of a selected item.
248+
*
249+
* @return void
250+
* @private
251+
*/
252+
_clearActiveState: function () {
253+
this.element.find('li').removeClass('active has-active');
241254
},
242255

243256
/**

0 commit comments

Comments
 (0)