Improve menu images, mobile links and sprite caching
CI / PHP lint (8.1) (push) Successful in 2m9s
CI / PHP lint (8.5) (push) Successful in 1m35s
CI / Coding standards (push) Successful in 1m7s

This commit is contained in:
Tiamak
2026-07-22 23:18:05 +02:00
parent 7236d17ead
commit 74618146c9
8 changed files with 190 additions and 47 deletions
+63 -15
View File
@@ -144,7 +144,8 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
$output = $this->postProcess(); $output = $this->postProcess();
$this->context->controller->addCSS($this->_path . 'views/css/admin.css'); $this->context->controller->addCSS($this->_path . 'views/css/admin.css');
$this->context->controller->addJS($this->_path . 'views/js/admin.js'); $adminJsPath = $this->getLocalPath() . 'views/js/admin.js';
$this->context->controller->addJS($this->_path . 'views/js/admin.js?v=' . (is_file($adminJsPath) ? (string) filemtime($adminJsPath) : (string) time()));
$languages = $this->context->controller->getLanguages(); $languages = $this->context->controller->getLanguages();
$repository = new MenuRepository(Db::getInstance(), $this); $repository = new MenuRepository(Db::getInstance(), $this);
@@ -155,7 +156,7 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
'advmegamenu_default_lang' => (int) Configuration::get('PS_LANG_DEFAULT'), 'advmegamenu_default_lang' => (int) Configuration::get('PS_LANG_DEFAULT'),
'advmegamenu_tree' => $tree, 'advmegamenu_tree' => $tree,
'advmegamenu_ajax_url' => $this->getAdminAjaxUrl(), 'advmegamenu_ajax_url' => $this->getAdminAjaxUrl(),
'advmegamenu_admin_token' => Tools::getAdminTokenLite('AdminModules'), 'advmegamenu_admin_token' => $this->getAjaxToken(),
]); ]);
return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl'); return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl');
@@ -197,31 +198,42 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
return ''; return '';
} }
$menuCssFile = $this->getLocalPath() . 'views/css/megamenu.css';
$menuJsFile = $this->getLocalPath() . 'views/js/megamenu.js';
$controller->registerStylesheet( $controller->registerStylesheet(
'module-advancedmegamenu-front', 'module-advancedmegamenu-front',
$this->_path . 'views/css/megamenu.css', $this->_path . 'views/css/megamenu.css',
['media' => 'all', 'priority' => 150] [
'media' => 'all',
'priority' => 150,
'version' => is_file($menuCssFile) ? (string) filemtime($menuCssFile) : $this->version,
]
); );
$controller->registerJavascript( $controller->registerJavascript(
'module-advancedmegamenu-front', 'module-advancedmegamenu-front',
$this->_path . 'views/js/megamenu.js', $this->_path . 'views/js/megamenu.js',
['position' => 'bottom', 'priority' => 150] [
'position' => 'bottom',
'priority' => 150,
'version' => is_file($menuJsFile) ? (string) filemtime($menuJsFile) : $this->version,
]
); );
$spriteCssFile = $this->getLocalPath() . 'views/css/generated/menu-sprite.css'; $spriteCssFile = $this->getLocalPath() . 'views/css/generated/menu-sprite.css';
if (is_file($spriteCssFile)) { if (is_file($spriteCssFile)) {
$spriteCssUrl = $this->_path . 'views/css/generated/menu-sprite.css'; $spriteCssUrl = $this->_path . 'views/css/generated/menu-sprite.css';
$spriteImageFile = $this->getLocalPath() . 'views/img/generated/menu-sprite.webp'; $spriteImageFile = $this->getCurrentSpriteImageFile($spriteCssFile);
$controller->registerStylesheet( $controller->registerStylesheet(
'module-advancedmegamenu-sprite', 'module-advancedmegamenu-sprite',
$spriteCssUrl, $spriteCssUrl,
['media' => 'all', 'priority' => 151] ['media' => 'all', 'priority' => 151, 'version' => (string) filemtime($spriteCssFile)]
); );
$preloadLinks = []; $preloadLinks = [];
if (is_file($spriteImageFile)) { if ($spriteImageFile && is_file($spriteImageFile)) {
$spriteImageUrl = $this->_path . 'views/img/generated/menu-sprite.webp?v=' . (string) filemtime($spriteImageFile); $spriteImageUrl = $this->_path . 'views/img/generated/' . basename($spriteImageFile);
$preloadLinks[] = '<link rel="preload" href="' . htmlspecialchars($spriteImageUrl, ENT_QUOTES, 'UTF-8') . '" as="image" type="image/webp">'; $preloadLinks[] = '<link rel="preload" href="' . htmlspecialchars($spriteImageUrl, ENT_QUOTES, 'UTF-8') . '" as="image" type="image/webp">';
} }
@@ -406,15 +418,22 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
private function assertAdminToken(): void private function assertAdminToken(): void
{ {
$token = (string) Tools::getValue('token'); $token = (string) Tools::getValue('token');
$expectedToken = Tools::getAdminTokenLite('AdminModules'); $validTokens = array_filter([
$this->getAjaxToken(),
Tools::getAdminTokenLite('AdminModules'),
]);
if (!hash_equals($expectedToken, $token)) { foreach ($validTokens as $validToken) {
header('Content-Type: application/json', true, 403); if (hash_equals((string) $validToken, $token)) {
exit(json_encode([ return;
'error' => true, }
'message' => $this->trans('Invalid admin token.', [], 'Modules.Advancedmegamenu.Admin'),
]));
} }
header('Content-Type: application/json', true, 403);
exit(json_encode([
'error' => true,
'message' => $this->trans('Invalid admin token.', [], 'Modules.Advancedmegamenu.Admin'),
]));
} }
private function ajaxSearchProducts(): void private function ajaxSearchProducts(): void
@@ -615,6 +634,28 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
}, $icons)); }, $icons));
} }
private function getCurrentSpriteImageFile(string $spriteCssFile): string
{
$css = (string) @file_get_contents($spriteCssFile);
if (preg_match('#img/generated/([^")]+\.webp)#', $css, $matches)) {
$spriteImageFile = $this->getLocalPath() . 'views/img/generated/' . basename($matches[1]);
if (is_file($spriteImageFile)) {
return $spriteImageFile;
}
}
$spriteFiles = glob($this->getLocalPath() . 'views/img/generated/menu-sprite*.webp') ?: [];
if (empty($spriteFiles)) {
return '';
}
usort($spriteFiles, static function (string $left, string $right): int {
return filemtime($right) <=> filemtime($left);
});
return (string) $spriteFiles[0];
}
/** /**
* @param array<string, mixed> $node * @param array<string, mixed> $node
* *
@@ -970,6 +1011,13 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
]); ]);
} }
private function getAjaxToken(): string
{
$employeeId = isset($this->context->employee) ? (int) $this->context->employee->id : 0;
return Tools::hash($this->name . '|ajax|' . $employeeId);
}
private function buildModuleImageUrl(string $path): string private function buildModuleImageUrl(string $path): string
{ {
return rtrim($this->getPathUri(), '/') . '/views/img/' . ltrim($path, '/'); return rtrim($this->getPathUri(), '/') . '/views/img/' . ltrim($path, '/');
+29 -13
View File
@@ -22,7 +22,6 @@ class SpriteGenerator
{ {
$imageOutputDir = $this->moduleDir . '/views/img/generated'; $imageOutputDir = $this->moduleDir . '/views/img/generated';
$cssOutputDir = $this->moduleDir . '/views/css/generated'; $cssOutputDir = $this->moduleDir . '/views/css/generated';
$spritePath = $imageOutputDir . '/menu-sprite.webp';
$cssPath = $cssOutputDir . '/menu-sprite.css'; $cssPath = $cssOutputDir . '/menu-sprite.css';
if (!is_dir($imageOutputDir)) { if (!is_dir($imageOutputDir)) {
@@ -34,7 +33,7 @@ class SpriteGenerator
} }
if (empty($icons)) { if (empty($icons)) {
$this->cleanup($spritePath, $cssPath); $this->cleanup($imageOutputDir, $cssPath);
return; return;
} }
@@ -77,7 +76,7 @@ class SpriteGenerator
} }
if (empty($images) || $totalWidth === 0 || $maxHeight === 0) { if (empty($images) || $totalWidth === 0 || $maxHeight === 0) {
$this->cleanup($spritePath, $cssPath); $this->cleanup($imageOutputDir, $cssPath);
return; return;
} }
@@ -101,22 +100,37 @@ class SpriteGenerator
); );
} }
$spriteWritten = imagewebp($sprite, $spritePath, 85); $tmpSpritePath = $imageOutputDir . '/menu-sprite.tmp.webp';
$spriteWritten = imagewebp($sprite, $tmpSpritePath, 85);
imagedestroy($sprite); imagedestroy($sprite);
if (!$spriteWritten || !is_file($spritePath)) { if (!$spriteWritten || !is_file($tmpSpritePath)) {
foreach ($images as $iconImage) { foreach ($images as $iconImage) {
imagedestroy($iconImage['resource']); imagedestroy($iconImage['resource']);
} }
$this->cleanup($spritePath, $cssPath); $this->cleanup($imageOutputDir, $cssPath);
return;
}
$hash = substr((string) sha1_file($tmpSpritePath), 0, 16);
$spriteFilename = 'menu-sprite-' . $hash . '.webp';
$spritePath = $imageOutputDir . '/' . $spriteFilename;
if (!rename($tmpSpritePath, $spritePath)) {
@unlink($tmpSpritePath);
foreach ($images as $iconImage) {
imagedestroy($iconImage['resource']);
}
$this->cleanup($imageOutputDir, $cssPath);
return; return;
} }
$version = is_file($spritePath) ? (string) filemtime($spritePath) : (string) time();
$css = []; $css = [];
$css[] = '.adv-megamenu__icon{display:inline-block;background-repeat:no-repeat;background-image:url("../../img/generated/menu-sprite.webp?v=' . $version . '");}'; $css[] = '.adv-megamenu__icon{display:inline-block;background-repeat:no-repeat;background-image:url("../../img/generated/' . $spriteFilename . '");}';
foreach ($images as $iconImage) { foreach ($images as $iconImage) {
$css[] = sprintf( $css[] = sprintf(
@@ -132,15 +146,17 @@ class SpriteGenerator
file_put_contents($cssPath, implode(PHP_EOL, $css) . PHP_EOL); file_put_contents($cssPath, implode(PHP_EOL, $css) . PHP_EOL);
} }
private function cleanup(string $spritePath, string $cssPath): void private function cleanup(string $imageOutputDir, string $cssPath): void
{ {
if (is_file($spritePath)) {
unlink($spritePath);
}
if (is_file($cssPath)) { if (is_file($cssPath)) {
unlink($cssPath); unlink($cssPath);
} }
foreach (glob($imageOutputDir . '/menu-sprite*.webp') ?: [] as $spritePath) {
if (is_file($spritePath)) {
unlink($spritePath);
}
}
} }
/** /**
+13
View File
@@ -211,6 +211,19 @@
border: 1px solid #edf1f4; border: 1px solid #edf1f4;
} }
.adv-menu-image-preview-wrap {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.adv-menu-image-remove {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.adv-menu-admin-node__collections { .adv-menu-admin-node__collections {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
+25
View File
@@ -569,6 +569,31 @@
font-weight: 700; font-weight: 700;
} }
.adv-megamenu .adv-megamenu__mobile-title-link,
.adv-megamenu .adv-megamenu__mobile-title-link:link,
.adv-megamenu .adv-megamenu__mobile-title-link:visited {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 0.25rem;
min-width: 0;
color: #8a3c1f;
font-weight: 600;
line-height: 1.25;
text-align: right;
text-decoration: none;
}
.adv-megamenu__mobile-title-link .material-icons {
flex: 0 0 auto;
font-size: 1.125rem;
}
.adv-megamenu .adv-megamenu__mobile-title-link:hover,
.adv-megamenu .adv-megamenu__mobile-title-link:focus-visible {
color: #8a3c1f;
}
.adv-megamenu__mobile-viewport { .adv-megamenu__mobile-viewport {
position: relative; position: relative;
height: calc(100% - 56px); height: calc(100% - 56px);
+51 -10
View File
@@ -458,7 +458,12 @@
var rootHint = nodeModal.querySelector('.js-node-root-hint'); var rootHint = nodeModal.querySelector('.js-node-root-hint');
var imageGroup = nodeModal.querySelector('.js-node-image-group'); var imageGroup = nodeModal.querySelector('.js-node-image-group');
var previewWrap = nodeModal.querySelector('.js-node-icon-preview'); var previewWrap = nodeModal.querySelector('.js-node-icon-preview');
renderImagePreview(previewWrap, depth > 0 ? node.icon_url : ''); renderImagePreview(previewWrap, depth > 0 ? node.icon_url : '', function () {
node.icon_path = '';
node.icon_url = '';
renderImagePreview(previewWrap, '');
serializeTree();
});
rootHint.style.display = depth === 0 ? 'block' : 'none'; rootHint.style.display = depth === 0 ? 'block' : 'none';
rootHint.textContent = t('rootOnlyHint', 'For first-level items only title and custom link are edited here.'); rootHint.textContent = t('rootOnlyHint', 'For first-level items only title and custom link are edited here.');
@@ -473,7 +478,12 @@
uploadImage(event.target.files[0], 'menu_icon', function (response) { uploadImage(event.target.files[0], 'menu_icon', function (response) {
node.icon_path = response.path; node.icon_path = response.path;
node.icon_url = response.url; node.icon_url = response.url;
renderImagePreview(previewWrap, response.url); renderImagePreview(previewWrap, response.url, function () {
node.icon_path = '';
node.icon_url = '';
renderImagePreview(previewWrap, '');
serializeTree();
});
serializeTree(); serializeTree();
}, { }, {
menuDepth: depth, menuDepth: depth,
@@ -572,7 +582,13 @@
layoutModal.querySelector('.js-layout-type').value = layout.block_type || 'promo'; layoutModal.querySelector('.js-layout-type').value = layout.block_type || 'promo';
layoutModal.querySelector('.js-layout-title').checked = !!layout.show_title; layoutModal.querySelector('.js-layout-title').checked = !!layout.show_title;
layoutModal.querySelector('.js-layout-bg').value = layout.background_color || ''; layoutModal.querySelector('.js-layout-bg').value = layout.background_color || '';
renderImagePreview(layoutModal.querySelector('.js-layout-preview'), layout.custom_image_url); var layoutPreviewWrap = layoutModal.querySelector('.js-layout-preview');
renderImagePreview(layoutPreviewWrap, layout.custom_image_url, function () {
layout.custom_image = '';
layout.custom_image_url = '';
renderImagePreview(layoutPreviewWrap, '');
serializeTree();
});
renderProducts(layout.products, layoutModal.querySelector('.js-layout-products')); renderProducts(layout.products, layoutModal.querySelector('.js-layout-products'));
layoutModal.querySelector('.js-layout-product-results').innerHTML = ''; layoutModal.querySelector('.js-layout-product-results').innerHTML = '';
layoutModal.querySelector('.js-layout-product-search').value = ''; layoutModal.querySelector('.js-layout-product-search').value = '';
@@ -583,7 +599,12 @@
uploadImage(event.target.files[0], 'layout_image', function (response) { uploadImage(event.target.files[0], 'layout_image', function (response) {
layout.custom_image = response.path; layout.custom_image = response.path;
layout.custom_image_url = response.url; layout.custom_image_url = response.url;
renderImagePreview(layoutModal.querySelector('.js-layout-preview'), response.url); renderImagePreview(layoutPreviewWrap, response.url, function () {
layout.custom_image = '';
layout.custom_image_url = '';
renderImagePreview(layoutPreviewWrap, '');
serializeTree();
});
serializeTree(); serializeTree();
}); });
}; };
@@ -678,16 +699,30 @@
showModal(confirmModal); showModal(confirmModal);
} }
function renderImagePreview(wrap, url) { function renderImagePreview(wrap, url, onRemove) {
wrap.innerHTML = ''; wrap.innerHTML = '';
if (!url) { if (!url) {
return; return;
} }
var preview = document.createElement('div');
preview.className = 'adv-menu-image-preview-wrap';
var image = document.createElement('img'); var image = document.createElement('img');
image.src = url; image.src = url;
image.className = 'adv-menu-image-preview'; image.className = 'adv-menu-image-preview';
wrap.appendChild(image); preview.appendChild(image);
if (typeof onRemove === 'function') {
var removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-default btn-sm adv-menu-image-remove';
removeButton.innerHTML = '<i class="icon-trash"></i><span>' + escapeHtml(t('removeImage', 'Remove image')) + '</span>';
removeButton.addEventListener('click', onRemove);
preview.appendChild(removeButton);
}
wrap.appendChild(preview);
} }
function getNodeTitle(node) { function getNodeTitle(node) {
@@ -727,9 +762,6 @@
var uploadOptions = options || {}; var uploadOptions = options || {};
var formData = new FormData(); var formData = new FormData();
formData.append('ajax', '1');
formData.append('action', 'uploadImage');
formData.append('token', token);
formData.append('preset', preset || 'default'); formData.append('preset', preset || 'default');
if (typeof uploadOptions.menuDepth !== 'undefined') { if (typeof uploadOptions.menuDepth !== 'undefined') {
formData.append('menu_depth', String(uploadOptions.menuDepth)); formData.append('menu_depth', String(uploadOptions.menuDepth));
@@ -739,17 +771,26 @@
} }
formData.append('image', file); formData.append('image', file);
fetch(ajaxUrl, { fetch(ajaxUrl + '&ajax=1&action=uploadImage&token=' + encodeURIComponent(token), {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(function (response) { .then(function (response) {
if (!response.ok) {
throw new Error('Upload failed: HTTP ' + response.status);
}
return response.json(); return response.json();
}) })
.then(function (payload) { .then(function (payload) {
if (payload && !payload.error) { if (payload && !payload.error) {
callback(payload); callback(payload);
return;
} }
throw new Error(payload && payload.message ? payload.message : 'Upload failed');
})
.catch(function (error) {
window.alert(error.message || 'Upload failed');
}); });
} }
-8
View File
@@ -219,14 +219,6 @@
return; return;
} }
item.addEventListener('mouseenter', function () {
if (!isDesktop()) {
return;
}
activateSubmenuPane(item);
});
bindDesktopClickIntent(link, function () { bindDesktopClickIntent(link, function () {
activateSubmenuPane(item); activateSubmenuPane(item);
}); });
+1
View File
@@ -43,6 +43,7 @@
"description": "{l s='Description' d='Modules.Advancedmegamenu.Admin' js=1}", "description": "{l s='Description' d='Modules.Advancedmegamenu.Admin' js=1}",
"customLinkLabel": "{l s='Custom link' d='Modules.Advancedmegamenu.Admin' js=1}", "customLinkLabel": "{l s='Custom link' d='Modules.Advancedmegamenu.Admin' js=1}",
"iconUpload": "{l s='Item image/icon' d='Modules.Advancedmegamenu.Admin' js=1}", "iconUpload": "{l s='Item image/icon' d='Modules.Advancedmegamenu.Admin' js=1}",
"removeImage": "{l s='Remove image' d='Modules.Advancedmegamenu.Admin' js=1}",
"rootOnlyHint": "{l s='For first-level items only title and custom link are edited here.' d='Modules.Advancedmegamenu.Admin' js=1}", "rootOnlyHint": "{l s='For first-level items only title and custom link are edited here.' d='Modules.Advancedmegamenu.Admin' js=1}",
"width": "{l s='Width' d='Modules.Advancedmegamenu.Admin' js=1}", "width": "{l s='Width' d='Modules.Advancedmegamenu.Admin' js=1}",
"blockType": "{l s='Block type' d='Modules.Advancedmegamenu.Admin' js=1}", "blockType": "{l s='Block type' d='Modules.Advancedmegamenu.Admin' js=1}",
+8 -1
View File
@@ -174,7 +174,14 @@
<button type="button" class="adv-megamenu__mobile-back js-adv-back"> <button type="button" class="adv-megamenu__mobile-back js-adv-back">
&lt; {l s='Wstecz' d='Modules.Advancedmegamenu.Shop'} &lt; {l s='Wstecz' d='Modules.Advancedmegamenu.Shop'}
</button> </button>
<span>{$node.title|escape:'htmlall':'UTF-8'}</span> {if $node.url && $node.url != '#'}
<a href="{$node.url|escape:'htmlall':'UTF-8'}" class="adv-megamenu__mobile-title-link" {if $node.new_window}target="_blank" rel="noopener"{/if}>
<span>{$node.title|escape:'htmlall':'UTF-8'}</span>
<span class="material-icons" aria-hidden="true">arrow_forward</span>
</a>
{else}
<span class="adv-megamenu__mobile-title">{$node.title|escape:'htmlall':'UTF-8'}</span>
{/if}
</div> </div>
<ul class="adv-megamenu__mobile-list"> <ul class="adv-megamenu__mobile-list">
{foreach from=$node.category_branch item=branch} {foreach from=$node.category_branch item=branch}