forked from UKSOURCE/cms.hailearning.edu.vn
356 lines
15 KiB
Plaintext
356 lines
15 KiB
Plaintext
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-sitemap me-2"></i>Menu Structure
|
|
</h6>
|
|
<button class="btn btn-primary btn-sm" onclick="prepareAddMenu()">
|
|
<i class="fas fa-plus me-1"></i>Add Root Menu
|
|
</button>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="nestedMenuContainer" class="nested-menu-container">
|
|
<ul id="menuRoot" class="list-unstyled menu-group" data-id="root">
|
|
<% function renderMenu(items) { %>
|
|
<% items.forEach(item => { %>
|
|
<li class="menu-item-wrapper mb-2" data-id="<%= item._id %>">
|
|
<div class="menu-item-row d-flex align-items-center p-3 rounded border hover-shadow-sm bg-white">
|
|
<div class="menu-drag-handle me-2 text-muted">
|
|
<i class="fas fa-grip-vertical"></i>
|
|
</div>
|
|
|
|
<div class="menu-toggle-icon me-2">
|
|
<% if (item.children && item.children.length > 0) { %>
|
|
<button class="btn btn-sm p-0 btn-toggle-nested" type="button">
|
|
<i class="fas fa-chevron-down transition-base text-muted" style="width: 12px;"></i>
|
|
</button>
|
|
<% } else { %>
|
|
<div style="width: 24px;"></div>
|
|
<% } %>
|
|
</div>
|
|
|
|
<div class="menu-icon me-3">
|
|
<i class="fas <%= (item.type === 'external' ? 'fa-external-link-alt text-info' : 'fa-link text-secondary') %>"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex align-items-center">
|
|
<span class="fw-bold text-dark"><%= item.title %></span>
|
|
<% if (item.type === 'external') { %>
|
|
<span class="badge bg-light text-dark border ms-2" style="font-size: 0.7rem;">External</span>
|
|
<% } %>
|
|
<% if (item.status === 'inactive') { %>
|
|
<span class="badge bg-soft-secondary ms-2">Inactive</span>
|
|
<% } else { %>
|
|
<span class="badge bg-soft-success ms-2">Active</span>
|
|
<% } %>
|
|
</div>
|
|
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
|
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
|
</div>
|
|
</div>
|
|
<div class="menu-actions">
|
|
<div class="btn-group-action">
|
|
<button class="btn btn-sm btn-add-child" data-id="<%= item._id %>" title="Add Sub-menu">
|
|
<i class="fas fa-plus text-action-add"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-edit-menu" data-item='<%= JSON.stringify(item).replace(/'/g, "'") %>' title="Edit">
|
|
<i class="fas fa-edit text-action-edit"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-delete-menu" data-id="<%= item._id %>" data-title="<%= item.title %>" title="Delete">
|
|
<i class="fas fa-trash text-action-delete"></i>
|
|
</button>
|
|
</div>
|
|
<form id="delete-form-<%= item._id %>" action="/admin/header/menu/delete" method="POST" class="d-none">
|
|
<input type="hidden" name="id" value="<%= item._id %>">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<ul class="list-unstyled menu-group mt-2 ps-4 ms-2 border-start nested-list" style="min-height: 5px;" data-id="<%= item._id %>">
|
|
<% if (item.children && item.children.length > 0) { %>
|
|
<% renderMenu(item.children) %>
|
|
<% } %>
|
|
</ul>
|
|
</li>
|
|
<% }) %>
|
|
<% } %>
|
|
<% if (menuData.tree && menuData.tree.length > 0) { %>
|
|
<% renderMenu(menuData.tree) %>
|
|
<% } %>
|
|
</ul>
|
|
<% if (!menuData.tree || menuData.tree.length === 0) { %>
|
|
<div class="text-center py-5">
|
|
<img src="/assets/img/icon/empty-state.svg" alt="Empty" style="width: 120px; opacity: 0.5;" class="mb-3">
|
|
<p class="text-muted">No menu items found. Start by adding a root menu.</p>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('Menu Tab JS Initialized');
|
|
initSortable();
|
|
|
|
function initSortable() {
|
|
const menuGroups = document.querySelectorAll('.menu-group');
|
|
menuGroups.forEach(group => {
|
|
new Sortable(group, {
|
|
group: 'nested-menu',
|
|
animation: 150,
|
|
fallbackOnBody: true,
|
|
swapThreshold: 0.65,
|
|
handle: '.menu-drag-handle',
|
|
ghostClass: 'menu-ghost',
|
|
chosenClass: 'menu-chosen',
|
|
dragClass: 'menu-dragging',
|
|
onEnd: function (evt) {
|
|
console.log('Drag ended', evt);
|
|
// Highlight that changes need saving
|
|
const saveBtn = document.getElementById('saveHeaderBtn');
|
|
if (saveBtn && typeof window.markHeaderChanged === 'function') {
|
|
window.markHeaderChanged();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Bind Edit/Add/Delete buttons
|
|
bindMenuActions();
|
|
|
|
function bindMenuActions() {
|
|
// Use event delegation for better performance and to handle re-rendered items if any
|
|
const container = document.getElementById('nestedMenuContainer');
|
|
if (container) {
|
|
container.addEventListener('click', function(e) {
|
|
const editBtn = e.target.closest('.btn-edit-menu');
|
|
if (editBtn) {
|
|
try {
|
|
const item = JSON.parse(editBtn.dataset.item);
|
|
console.log('=== TRACE: Edit Menu Clicked ===', item);
|
|
prepareEditMenu(item);
|
|
} catch (e) {
|
|
console.error('Error parsing menu item data:', e);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const addChildBtn = e.target.closest('.btn-add-child');
|
|
if (addChildBtn) {
|
|
const pid = addChildBtn.dataset.id;
|
|
console.log('=== TRACE: Add Child Clicked ===', { pid });
|
|
if (typeof prepareAddChild === 'function') {
|
|
prepareAddChild(pid);
|
|
} else {
|
|
console.error('prepareAddChild function not found');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const deleteBtn = e.target.closest('.btn-delete-menu');
|
|
if (deleteBtn) {
|
|
const id = deleteBtn.dataset.id;
|
|
const title = deleteBtn.dataset.title;
|
|
console.log('=== TRACE: Delete Menu Clicked ===', { id, title });
|
|
if (confirm(`Are you sure you want to delete "${title}" and all its sub-menu items?`)) {
|
|
const form = document.getElementById('delete-form-' + id);
|
|
console.log('=== TRACE: Submitting Delete Form ===', form ? form.action : 'FORM NOT FOUND');
|
|
if (form) form.submit();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const toggleBtn = e.target.closest('.btn-toggle-nested');
|
|
if (toggleBtn) {
|
|
const wrapper = toggleBtn.closest('.menu-item-wrapper');
|
|
const nestedUl = wrapper.querySelector('.nested-list');
|
|
const icon = toggleBtn.querySelector('i');
|
|
|
|
if (nestedUl) {
|
|
const isCollapsed = nestedUl.classList.toggle('collapsed');
|
|
if (isCollapsed) {
|
|
icon.style.transform = 'rotate(-90deg)';
|
|
} else {
|
|
icon.style.transform = 'rotate(0deg)';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
function prepareAddMenu() {
|
|
console.log('=== TRACE: prepareAddMenu Called ===');
|
|
const form = document.getElementById('menuForm');
|
|
if (!form) return;
|
|
form.action = '/admin/header/menu/create';
|
|
document.getElementById('modalTitle').innerText = 'Add Root Menu';
|
|
document.getElementById('menuId').value = '';
|
|
document.getElementById('parentId').value = '';
|
|
document.getElementById('formTitle').value = '';
|
|
document.getElementById('formUrl').value = '';
|
|
document.getElementById('formOrder').value = '0';
|
|
document.getElementById('formStatus').value = 'active';
|
|
document.getElementById('typeInternal').checked = true;
|
|
|
|
const modalElement = document.getElementById('modalAddMenu');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
|
modal.show();
|
|
}
|
|
|
|
function prepareAddChild(parentId) {
|
|
prepareAddMenu();
|
|
document.getElementById('parentId').value = parentId;
|
|
document.getElementById('modalTitle').innerText = 'Add Sub-Menu Item';
|
|
}
|
|
|
|
function prepareEditMenu(item) {
|
|
const form = document.getElementById('menuForm');
|
|
if (!form) return;
|
|
form.action = '/admin/header/menu/update/' + item._id;
|
|
document.getElementById('modalTitle').innerText = 'Edit Menu Item';
|
|
document.getElementById('menuId').value = item._id;
|
|
document.getElementById('parentId').value = item.parentId || '';
|
|
document.getElementById('formTitle').value = item.title;
|
|
document.getElementById('formUrl').value = item.url;
|
|
document.getElementById('formOrder').value = item.order;
|
|
document.getElementById('formStatus').value = item.status;
|
|
|
|
if (item.type === 'external') {
|
|
document.getElementById('typeExternal').checked = true;
|
|
} else {
|
|
document.getElementById('typeInternal').checked = true;
|
|
}
|
|
|
|
const modalElement = document.getElementById('modalAddMenu');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
|
modal.show();
|
|
}
|
|
|
|
function collectMenuData() {
|
|
const items = [];
|
|
function traverse(element, parentId = null) {
|
|
const children = element.children;
|
|
for (let i = 0; i < children.length; i++) {
|
|
const li = children[i];
|
|
if (li.tagName !== 'LI') continue;
|
|
|
|
const id = li.dataset.id;
|
|
if (!id) continue;
|
|
|
|
items.push({
|
|
id: id,
|
|
order: i + 1,
|
|
parentId: parentId === 'root' ? null : parentId
|
|
});
|
|
|
|
const subUl = li.querySelector('.menu-group');
|
|
if (subUl) {
|
|
traverse(subUl, id);
|
|
}
|
|
}
|
|
}
|
|
const rootUl = document.getElementById('menuRoot');
|
|
if (rootUl) traverse(rootUl, 'root');
|
|
return items;
|
|
}
|
|
|
|
window.saveMenuChanges = function(showToastFlag = true) {
|
|
return new Promise((resolve, reject) => {
|
|
console.log('=== TRACE: saveMenuChanges Called (Reorder) ===');
|
|
const items = collectMenuData();
|
|
|
|
if (items.length === 0) {
|
|
console.warn('No menu items found to reorder');
|
|
return resolve({ success: true, message: 'No items' });
|
|
}
|
|
|
|
const saveBtn = document.getElementById('saveHeaderBtn');
|
|
const originalHtml = saveBtn ? saveBtn.innerHTML : '';
|
|
|
|
// Only manage button state if this is a direct call (not unified save)
|
|
const manageButton = showToastFlag && saveBtn;
|
|
|
|
if (manageButton) {
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving Menu...';
|
|
}
|
|
|
|
fetch('/admin/header/menu/reorder', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ items: items })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
if (showToastFlag) showNotification('Menu structure saved', 'success');
|
|
resolve(data);
|
|
} else {
|
|
reject(new Error(data.message));
|
|
}
|
|
})
|
|
.catch(error => reject(error))
|
|
.finally(() => {
|
|
if (manageButton) {
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = originalHtml;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
window.collectMenuData = collectMenuData;
|
|
|
|
// Global reset fallback
|
|
window.prepareAddMenu = prepareAddMenu;
|
|
</script>
|
|
|
|
<!-- Redundant buttons removed to use global buttons in index.ejs -->
|
|
|
|
<style>
|
|
.nested-menu-container {
|
|
padding: var(--spacing-3);
|
|
}
|
|
.menu-group {
|
|
min-height: 10px;
|
|
}
|
|
.menu-item-row {
|
|
transition: var(--transition-base);
|
|
}
|
|
.menu-drag-handle { cursor: grab; padding: 5px; }
|
|
.menu-drag-handle:active { cursor: grabbing; }
|
|
|
|
/* SortableJS Classes mapped to our variables */
|
|
.menu-ghost {
|
|
opacity: 0.4;
|
|
border: 2px dashed var(--primary-color) !important;
|
|
}
|
|
.menu-chosen {
|
|
background-color: var(--primary-soft) !important;
|
|
}
|
|
.menu-dragging {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* Collapse/Expand Styles */
|
|
.nested-list {
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
max-height: 2000px; /* Large enough for nested items */
|
|
}
|
|
.nested-list.collapsed {
|
|
max-height: 0;
|
|
margin-top: 0 !important;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.btn-toggle-nested i {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.transition-base {
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
</style>
|