feat: enhance mega menu navigation with dynamic image updates and support for nested level-4 content items

This commit is contained in:
2026-08-11 11:00:45 +07:00
parent 9ecde8d366
commit 3e69ec7d12
2 changed files with 138 additions and 14 deletions
@@ -215,6 +215,9 @@
<td><span class="badge badge-info">${tabCount} tabs</span></td>
<td><code>[plugin:tabbed-livestream id="${escapeHtml(group.id || '')}"]</code></td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-info mr-1" onclick="duplicateGroup(${idx})" title="Nhân bản">
<i class="fas fa-copy"></i>
</button>
<button type="button" class="btn btn-sm btn-primary mr-1" onclick="openEditGroupModal(${idx})" title="Chỉnh sửa">
<i class="fas fa-edit"></i> Sửa
</button>
@@ -269,6 +272,7 @@
<td><strong>${escapeHtml(tab.title || '')}</strong></td>
<td class="small text-truncate" style="max-width: 180px;">${escapeHtml(tab.excerpt || '')}</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-info mr-1" onclick="duplicateWorkingTab(${tIdx})" title="Nhân bản"><i class="fas fa-copy"></i></button>
<button type="button" class="btn btn-sm btn-secondary mr-1" onclick="openEditTabModal(${tIdx})"><i class="fas fa-pencil-alt"></i> Sửa</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteWorkingTab(${tIdx})"><i class="fas fa-times"></i></button>
</td>
@@ -347,6 +351,17 @@
}
}
// Duplicate tab in modal
function duplicateWorkingTab(tIdx) {
if (confirm('Bạn có chắc muốn nhân bản tab này?')) {
let originalTab = currentWorkingTabs[tIdx];
let duplicatedTab = JSON.parse(JSON.stringify(originalTab));
duplicatedTab.title = duplicatedTab.title + " (Copy)";
currentWorkingTabs.push(duplicatedTab);
renderModalTabsTable();
}
}
// Media picker inside tab modal
function openMediaPickerForModal() {
if (typeof SISMediaPicker !== 'undefined') {
@@ -398,6 +413,18 @@
}
}
// Duplicate group
function duplicateGroup(idx) {
if (confirm(`Bạn có chắc muốn nhân bản danh sách "${groups[idx].name}"?`)) {
let originalGroup = groups[idx];
let duplicatedGroup = JSON.parse(JSON.stringify(originalGroup));
duplicatedGroup.name = duplicatedGroup.name + " (Copy)";
duplicatedGroup.id = duplicatedGroup.id + "-copy-" + Math.random().toString(36).substring(2, 6);
groups.push(duplicatedGroup);
renderGroupsTable();
}
}
// Save form submission
function saveAllGroups() {
document.getElementById('groupDataInput').value = JSON.stringify(groups);
@@ -94,9 +94,22 @@
const firstL2 = menu.querySelector('.l2-hover-target');
if (firstL2) {
firstL2.classList.add('active');
const targetId = firstL2.getAttribute('data-target');
const targetId = firstL2.getAttribute('data-target') || firstL2.getAttribute('data-target-fallback');
const targetContent = document.getElementById(targetId);
if (targetContent) targetContent.style.display = 'block';
const imgElem = menu.querySelector('.mega-col-img-elem');
if (imgElem) {
const newImg = firstL2.getAttribute('data-image');
const defaultImg = imgElem.getAttribute('data-default-image');
const targetImg = newImg && newImg.trim() !== '' && newImg !== 'null' ? newImg : defaultImg;
imgElem.src = targetImg;
if (targetImg && targetImg.includes('logo.png')) {
imgElem.style = "object-fit: contain; background: white; padding: 20px; width: 100%; max-height: 245px;";
} else {
imgElem.style = "object-fit: cover; border-radius: 8px; width: 100%; max-height: 245px;";
}
}
}
});
@@ -111,13 +124,57 @@
// Hide all L3 content in this menu
wrapper.querySelectorAll('.l3-content').forEach(el => el.style.display = 'none');
// Hide all L4 content and reset image visibility
wrapper.querySelectorAll('.l4-content').forEach(el => el.style.display = 'none');
const imgWrapper = wrapper.querySelector('.mega-col-image-wrapper');
if (imgWrapper) imgWrapper.style.display = 'flex'; // image column has display: flex originally
wrapper.querySelectorAll('.l3-hover-target').forEach(el => el.classList.remove('active', 'font-weight-bold'));
// Activate current
this.classList.add('active');
const contentId = this.getAttribute('data-target');
const contentId = this.getAttribute('data-target') || this.getAttribute('data-target-fallback');
const content = document.getElementById(contentId);
if (content) {
content.style.display = 'block';
}
// Handle image override
const imgElem = wrapper.querySelector('.mega-col-img-elem');
if (imgElem) {
const newImg = this.getAttribute('data-image');
const defaultImg = imgElem.getAttribute('data-default-image');
const targetImg = newImg && newImg.trim() !== '' && newImg !== 'null' ? newImg : defaultImg;
imgElem.src = targetImg;
if (targetImg && targetImg.includes('logo.png')) {
imgElem.style = "object-fit: contain; background: white; padding: 20px; width: 100%; max-height: 245px;";
} else {
imgElem.style = "object-fit: cover; border-radius: 8px; width: 100%; max-height: 245px;";
}
}
});
});
document.querySelectorAll('.l3-hover-target').forEach(target => {
target.addEventListener('mouseenter', function() {
const wrapper = this.closest('.mega-menu-content');
if (!wrapper) return;
wrapper.querySelectorAll('.l3-hover-target').forEach(el => el.classList.remove('active', 'font-weight-bold'));
this.classList.add('active', 'font-weight-bold');
wrapper.querySelectorAll('.l4-content').forEach(el => el.style.display = 'none');
const hasChildren = this.getAttribute('data-has-children') === 'true';
const imgWrapper = wrapper.querySelector('.mega-col-image-wrapper');
if (hasChildren) {
const contentId = this.getAttribute('data-target');
const content = document.getElementById(contentId);
if (content) content.style.display = 'block';
if (imgWrapper) imgWrapper.style.display = 'none';
} else {
if (imgWrapper) imgWrapper.style.display = 'flex';
}
});
});
});
@@ -314,6 +371,32 @@
color: var(--color-white) !important;
}
/* Column 4: Level 4 */
.submenu-l4 {
list-style: none;
padding: 0;
margin: 0;
}
.submenu-l4 .menu-item-l4 {
padding: 0;
margin: 0;
}
.submenu-l4 .menu-item-l4 a {
display: block;
padding: 12px 15px;
color: #111827 !important;
text-decoration: none;
font-weight: 600;
font-size: 14px;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.2s;
}
.submenu-l4 .menu-item-l4 a:hover {
background-color: var(--color-brand) !important;
color: var(--color-white) !important;
}
/* Hide the mobile back button and title on desktop */
.mc--main-menu .megamenu.menu-item .submenu-wrapper .button-back,
.mc--main-menu .megamenu.menu-item .submenu-wrapper .navigation-title {
@@ -389,7 +472,7 @@
<!-- Column 2: Level 2 Links -->
<div class="mega-col-level2" th:if="${not #lists.isEmpty(item.children)}">
<ul class="submenu-l2">
<li class="menu-item-l2 l2-hover-target" th:each="child : ${item.children}" th:attr="data-target='l3-' + ${item.id} + '-' + ${child.id}">
<li class="menu-item-l2 l2-hover-target" th:each="child : ${item.children}" th:data-target-fallback="'l3-' + ${item.id} + '-' + ${child.id}" th:attr="data-target='l3-' + ${item.id} + '-' + ${child.id}" th:data-image="${child.imageUrl}">
<a th:href="${child.url}" th:text="${child.label}">Child Item</a>
</li>
</ul>
@@ -401,23 +484,37 @@
<th:block th:each="child : ${item.children}">
<ul th:id="'l3-' + ${item.id} + '-' + ${child.id}" class="submenu-l3 l3-content" style="display: none;">
<th:block th:if="${not #lists.isEmpty(child.children)}">
<li class="menu-item-l3" th:each="grandchild : ${child.children}">
<a th:href="${grandchild.url}" th:text="${grandchild.label}" th:classappend="${not #lists.isEmpty(grandchild.children) ? 'font-weight-bold' : ''}">Grandchild Item</a>
<ul th:if="${not #lists.isEmpty(grandchild.children)}" class="submenu-l4" style="padding-left: 15px; margin-top: 5px; list-style-type: none;">
<li class="menu-item-l4" th:each="greatgrandchild : ${grandchild.children}" style="margin-bottom: 5px;">
<a th:href="${greatgrandchild.url}" th:text="${greatgrandchild.label}" style="font-size: 13px; font-weight: normal; color: #555; text-transform: none;">Great-Grandchild Item</a>
</li>
</ul>
<li class="menu-item-l3 l3-hover-target" th:each="grandchild : ${child.children}" th:attr="data-target='l4-' + ${item.id} + '-' + ${child.id} + '-' + ${grandchild.id}" th:data-has-children="${not #lists.isEmpty(grandchild.children)}">
<a th:href="${grandchild.url}" th:text="${grandchild.label}">Grandchild Item</a>
</li>
</th:block>
</ul>
</th:block>
</div>
<!-- Column 4: Featured Image / Media Banner -->
<div class="mega-col-image">
<img th:if="${not #strings.isEmpty(item.imageUrl)}" th:src="${item.imageUrl}" style="object-fit: cover; border-radius: 8px; width: 100%; max-height: 245px;"/>
<img th:if="${#strings.isEmpty(item.imageUrl)}" th:src="@{/theme-assets/umass/images/logo.png}" style="object-fit: contain; background: white; padding: 20px; width: 100%; max-height: 245px;"/>
<!-- Column 4: Featured Image or Level 4 Links -->
<div class="mega-col-level4-and-image" style="flex: 1; border-left: 1px solid #d1d5db; padding-left: 20px;">
<!-- Level 4 Containers -->
<div class="level4-wrapper">
<th:block th:each="child : ${item.children}">
<th:block th:each="grandchild : ${child.children}">
<ul th:id="'l4-' + ${item.id} + '-' + ${child.id} + '-' + ${grandchild.id}" class="submenu-l4 l4-content" style="display: none; padding:0; margin:0; list-style:none;">
<li class="menu-item-l4" th:each="greatgrandchild : ${grandchild.children}">
<a th:href="${greatgrandchild.url}" th:text="${greatgrandchild.label}">Great-Grandchild Item</a>
</li>
</ul>
</th:block>
</th:block>
</div>
<!-- Featured Image -->
<div class="mega-col-image-wrapper mega-col-image" style="border-left: none; padding-left: 0;">
<img class="mega-col-img-elem"
th:data-default-image="${not #strings.isEmpty(item.imageUrl) ? item.imageUrl : '/theme-assets/umass/images/logo.png'}"
th:src="${not #strings.isEmpty(item.imageUrl) ? item.imageUrl : '/theme-assets/umass/images/logo.png'}"
th:style="${not #strings.isEmpty(item.imageUrl) ? 'object-fit: cover; border-radius: 8px; width: 100%; max-height: 245px;' : 'object-fit: contain; background: white; padding: 20px; width: 100%; max-height: 245px;'}"
/>
</div>
</div>
</div>
<!-- End Mega Menu Layout -->