Improve menu images, mobile links and sprite caching
This commit is contained in:
+59
-11
@@ -144,7 +144,8 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
|
||||
$output = $this->postProcess();
|
||||
|
||||
$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();
|
||||
$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_tree' => $tree,
|
||||
'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');
|
||||
@@ -197,31 +198,42 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
$menuCssFile = $this->getLocalPath() . 'views/css/megamenu.css';
|
||||
$menuJsFile = $this->getLocalPath() . 'views/js/megamenu.js';
|
||||
|
||||
$controller->registerStylesheet(
|
||||
'module-advancedmegamenu-front',
|
||||
$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(
|
||||
'module-advancedmegamenu-front',
|
||||
$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';
|
||||
if (is_file($spriteCssFile)) {
|
||||
$spriteCssUrl = $this->_path . 'views/css/generated/menu-sprite.css';
|
||||
$spriteImageFile = $this->getLocalPath() . 'views/img/generated/menu-sprite.webp';
|
||||
$spriteImageFile = $this->getCurrentSpriteImageFile($spriteCssFile);
|
||||
$controller->registerStylesheet(
|
||||
'module-advancedmegamenu-sprite',
|
||||
$spriteCssUrl,
|
||||
['media' => 'all', 'priority' => 151]
|
||||
['media' => 'all', 'priority' => 151, 'version' => (string) filemtime($spriteCssFile)]
|
||||
);
|
||||
|
||||
$preloadLinks = [];
|
||||
|
||||
if (is_file($spriteImageFile)) {
|
||||
$spriteImageUrl = $this->_path . 'views/img/generated/menu-sprite.webp?v=' . (string) filemtime($spriteImageFile);
|
||||
if ($spriteImageFile && is_file($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">';
|
||||
}
|
||||
|
||||
@@ -406,16 +418,23 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
|
||||
private function assertAdminToken(): void
|
||||
{
|
||||
$token = (string) Tools::getValue('token');
|
||||
$expectedToken = Tools::getAdminTokenLite('AdminModules');
|
||||
$validTokens = array_filter([
|
||||
$this->getAjaxToken(),
|
||||
Tools::getAdminTokenLite('AdminModules'),
|
||||
]);
|
||||
|
||||
foreach ($validTokens as $validToken) {
|
||||
if (hash_equals((string) $validToken, $token)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hash_equals($expectedToken, $token)) {
|
||||
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
|
||||
{
|
||||
@@ -615,6 +634,28 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
|
||||
}, $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
|
||||
*
|
||||
@@ -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
|
||||
{
|
||||
return rtrim($this->getPathUri(), '/') . '/views/img/' . ltrim($path, '/');
|
||||
|
||||
@@ -22,7 +22,6 @@ class SpriteGenerator
|
||||
{
|
||||
$imageOutputDir = $this->moduleDir . '/views/img/generated';
|
||||
$cssOutputDir = $this->moduleDir . '/views/css/generated';
|
||||
$spritePath = $imageOutputDir . '/menu-sprite.webp';
|
||||
$cssPath = $cssOutputDir . '/menu-sprite.css';
|
||||
|
||||
if (!is_dir($imageOutputDir)) {
|
||||
@@ -34,7 +33,7 @@ class SpriteGenerator
|
||||
}
|
||||
|
||||
if (empty($icons)) {
|
||||
$this->cleanup($spritePath, $cssPath);
|
||||
$this->cleanup($imageOutputDir, $cssPath);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +76,7 @@ class SpriteGenerator
|
||||
}
|
||||
|
||||
if (empty($images) || $totalWidth === 0 || $maxHeight === 0) {
|
||||
$this->cleanup($spritePath, $cssPath);
|
||||
$this->cleanup($imageOutputDir, $cssPath);
|
||||
|
||||
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);
|
||||
|
||||
if (!$spriteWritten || !is_file($spritePath)) {
|
||||
if (!$spriteWritten || !is_file($tmpSpritePath)) {
|
||||
foreach ($images as $iconImage) {
|
||||
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;
|
||||
}
|
||||
|
||||
$version = is_file($spritePath) ? (string) filemtime($spritePath) : (string) time();
|
||||
$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) {
|
||||
$css[] = sprintf(
|
||||
@@ -132,14 +146,16 @@ class SpriteGenerator
|
||||
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($cssPath)) {
|
||||
unlink($cssPath);
|
||||
}
|
||||
|
||||
foreach (glob($imageOutputDir . '/menu-sprite*.webp') ?: [] as $spritePath) {
|
||||
if (is_file($spritePath)) {
|
||||
unlink($spritePath);
|
||||
}
|
||||
|
||||
if (is_file($cssPath)) {
|
||||
unlink($cssPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,19 @@
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -569,6 +569,31 @@
|
||||
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 {
|
||||
position: relative;
|
||||
height: calc(100% - 56px);
|
||||
|
||||
+51
-10
@@ -458,7 +458,12 @@
|
||||
var rootHint = nodeModal.querySelector('.js-node-root-hint');
|
||||
var imageGroup = nodeModal.querySelector('.js-node-image-group');
|
||||
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.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) {
|
||||
node.icon_path = response.path;
|
||||
node.icon_url = response.url;
|
||||
renderImagePreview(previewWrap, response.url);
|
||||
renderImagePreview(previewWrap, response.url, function () {
|
||||
node.icon_path = '';
|
||||
node.icon_url = '';
|
||||
renderImagePreview(previewWrap, '');
|
||||
serializeTree();
|
||||
});
|
||||
serializeTree();
|
||||
}, {
|
||||
menuDepth: depth,
|
||||
@@ -572,7 +582,13 @@
|
||||
layoutModal.querySelector('.js-layout-type').value = layout.block_type || 'promo';
|
||||
layoutModal.querySelector('.js-layout-title').checked = !!layout.show_title;
|
||||
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'));
|
||||
layoutModal.querySelector('.js-layout-product-results').innerHTML = '';
|
||||
layoutModal.querySelector('.js-layout-product-search').value = '';
|
||||
@@ -583,7 +599,12 @@
|
||||
uploadImage(event.target.files[0], 'layout_image', function (response) {
|
||||
layout.custom_image = response.path;
|
||||
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();
|
||||
});
|
||||
};
|
||||
@@ -678,16 +699,30 @@
|
||||
showModal(confirmModal);
|
||||
}
|
||||
|
||||
function renderImagePreview(wrap, url) {
|
||||
function renderImagePreview(wrap, url, onRemove) {
|
||||
wrap.innerHTML = '';
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
var preview = document.createElement('div');
|
||||
preview.className = 'adv-menu-image-preview-wrap';
|
||||
|
||||
var image = document.createElement('img');
|
||||
image.src = url;
|
||||
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) {
|
||||
@@ -727,9 +762,6 @@
|
||||
|
||||
var uploadOptions = options || {};
|
||||
var formData = new FormData();
|
||||
formData.append('ajax', '1');
|
||||
formData.append('action', 'uploadImage');
|
||||
formData.append('token', token);
|
||||
formData.append('preset', preset || 'default');
|
||||
if (typeof uploadOptions.menuDepth !== 'undefined') {
|
||||
formData.append('menu_depth', String(uploadOptions.menuDepth));
|
||||
@@ -739,17 +771,26 @@
|
||||
}
|
||||
formData.append('image', file);
|
||||
|
||||
fetch(ajaxUrl, {
|
||||
fetch(ajaxUrl + '&ajax=1&action=uploadImage&token=' + encodeURIComponent(token), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed: HTTP ' + response.status);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (payload && !payload.error) {
|
||||
callback(payload);
|
||||
return;
|
||||
}
|
||||
throw new Error(payload && payload.message ? payload.message : 'Upload failed');
|
||||
})
|
||||
.catch(function (error) {
|
||||
window.alert(error.message || 'Upload failed');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -219,14 +219,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
item.addEventListener('mouseenter', function () {
|
||||
if (!isDesktop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
activateSubmenuPane(item);
|
||||
});
|
||||
|
||||
bindDesktopClickIntent(link, function () {
|
||||
activateSubmenuPane(item);
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"description": "{l s='Description' 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}",
|
||||
"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}",
|
||||
"width": "{l s='Width' d='Modules.Advancedmegamenu.Admin' js=1}",
|
||||
"blockType": "{l s='Block type' d='Modules.Advancedmegamenu.Admin' js=1}",
|
||||
|
||||
@@ -174,7 +174,14 @@
|
||||
<button type="button" class="adv-megamenu__mobile-back js-adv-back">
|
||||
< {l s='Wstecz' d='Modules.Advancedmegamenu.Shop'}
|
||||
</button>
|
||||
{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>
|
||||
<ul class="adv-megamenu__mobile-list">
|
||||
{foreach from=$node.category_branch item=branch}
|
||||
|
||||
Reference in New Issue
Block a user