Last active
July 20, 2025 19:01
-
-
Save zilveer/8279ef639393cf49ac7f85e9da052ecc to your computer and use it in GitHub Desktop.
Modal offvanvas router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Enhanced Routable Bootstrap Menus</title> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet"> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.1/font/bootstrap-icons.min.css" rel="stylesheet"> | |
<style> | |
.menu-container { | |
position: relative; | |
overflow: hidden; | |
width: 100%; | |
height: 100%; | |
} | |
.menu-level { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); | |
background: white; | |
transform: translateX(100%); | |
} | |
.menu-level.active { transform: translateX(0); } | |
.menu-level.slide-left { transform: translateX(-100%); } | |
.menu-item { | |
padding: 0.875rem 1.25rem; | |
border-bottom: 1px solid #f1f3f4; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
transition: background-color 0.2s ease; | |
} | |
.menu-item:hover { | |
background: #f8f9fa; | |
} | |
.menu-item:active { | |
background: #e9ecef; | |
} | |
.menu-footer { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
border-top: 1px solid #dee2e6; | |
padding: 1.25rem; | |
background: #f8f9fa; | |
} | |
/* Modal Enhancements */ | |
.modal-content { | |
height: 500px; | |
border-radius: 12px; | |
box-shadow: 0 10px 40px rgba(0,0,0,0.15); | |
} | |
.modal-body { | |
height: 100%; | |
padding: 0; | |
} | |
@media (max-width: 576px) { | |
.modal-dialog { | |
margin: 0; | |
max-width: none; | |
height: 100vh; | |
} | |
.modal-content { | |
height: 100vh; | |
border-radius: 0; | |
border: none; | |
} | |
} | |
/* Offcanvas Enhancements */ | |
.offcanvas-body { | |
height: 100%; | |
padding: 0; | |
} | |
/* Unified Header Styles */ | |
.menu-header { | |
position: relative; | |
min-height: 64px; | |
border-bottom: 1px solid #e9ecef; | |
padding: 0 1.25rem; | |
display: grid; | |
grid-template-columns: 40px 1fr 40px 40px; | |
align-items: center; | |
gap: 0.75rem; | |
background: white; | |
z-index: 1000; | |
} | |
.header-back { | |
background: none; | |
border: none; | |
color: #0d6efd; | |
cursor: pointer; | |
padding: 0.5rem; | |
font-size: 1.1rem; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 6px; | |
transition: background-color 0.2s ease; | |
opacity: 0; | |
pointer-events: none; | |
} | |
.header-back.visible { | |
opacity: 1; | |
pointer-events: all; | |
} | |
.header-back:hover { | |
background: rgba(13, 110, 253, 0.1); | |
} | |
.menu-title { | |
font-weight: 600; | |
font-size: 1.1rem; | |
text-align: center; | |
margin: 0; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
.header-action { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.header-close { | |
background: none; | |
border: none; | |
font-size: 1.25rem; | |
padding: 0.5rem; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: #6c757d; | |
border-radius: 6px; | |
transition: all 0.2s ease; | |
} | |
.header-close:hover { | |
background: rgba(0,0,0,0.1); | |
color: #495057; | |
} | |
/* Form Styles */ | |
.menu-form { | |
padding: 1.25rem; | |
height: calc(100% - 64px); | |
overflow-y: auto; | |
} | |
.form-section { | |
margin-bottom: 2rem; | |
} | |
.form-section:last-child { | |
margin-bottom: 0; | |
} | |
.form-section h6 { | |
font-weight: 600; | |
color: #495057; | |
margin-bottom: 1rem; | |
font-size: 0.9rem; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
} | |
.form-group { | |
margin-bottom: 1.5rem; | |
} | |
.form-label { | |
font-weight: 500; | |
color: #495057; | |
margin-bottom: 0.5rem; | |
} | |
.form-control:focus { | |
border-color: #0d6efd; | |
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); | |
} | |
.form-actions { | |
position: sticky; | |
bottom: 0; | |
background: white; | |
border-top: 1px solid #e9ecef; | |
padding: 1.25rem; | |
margin: 0 -1.25rem -1.25rem -1.25rem; | |
} | |
/* Loading States */ | |
.loading { | |
opacity: 0.6; | |
pointer-events: none; | |
} | |
.btn-loading { | |
position: relative; | |
} | |
.btn-loading::after { | |
content: ''; | |
position: absolute; | |
width: 16px; | |
height: 16px; | |
top: 50%; | |
left: 50%; | |
margin-left: -8px; | |
margin-top: -8px; | |
border: 2px solid transparent; | |
border-top: 2px solid currentColor; | |
border-radius: 50%; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
/* Toggle Switch Enhancements */ | |
.form-switch .form-check-input:checked { | |
background-color: #198754; | |
border-color: #198754; | |
} | |
/* Badge Styles */ | |
.badge { | |
font-size: 0.75rem; | |
font-weight: 500; | |
} | |
.badge-clickable { | |
cursor: pointer; | |
transition: all 0.2s ease; | |
} | |
.badge-clickable:hover { | |
transform: scale(1.05); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container mt-4"> | |
<h2>Enhanced Routable Bootstrap Menus</h2> | |
<div class="d-flex flex-wrap gap-3 mb-4"> | |
<button class="btn btn-primary" onclick="router.navigate('#modal/main')">Open Modal Menu</button> | |
<button class="btn btn-secondary" onclick="router.navigate('#offcanvas/settings')">Open Offcanvas Menu</button> | |
<button class="btn btn-success" onclick="router.navigate('#modal/profile-form')">Profile Form</button> | |
<button class="btn btn-info" onclick="router.navigate('#offcanvas/preferences-form')">Preferences</button> | |
</div> | |
<div class="row"> | |
<div class="col-md-6"> | |
<p><strong>Current route:</strong> <code id="currentRoute">#</code></p> | |
</div> | |
<div class="col-md-6"> | |
<p><strong>Form Data:</strong> <small id="formDataDisplay" class="text-muted">No form data saved</small></p> | |
</div> | |
</div> | |
</div> | |
<!-- Modal --> | |
<div class="modal fade" id="menuModal" tabindex="-1"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div id="modalHeader" class="menu-header"> | |
<button id="modalBackButton" class="header-back"> | |
<i class="bi bi-chevron-left"></i> | |
</button> | |
<h5 id="modalTitle" class="menu-title">Menu</h5> | |
<div id="modalHeaderAction" class="header-action"></div> | |
<button id="modalCloseButton" class="header-close"> | |
<i class="bi bi-x"></i> | |
</button> | |
</div> | |
<div class="modal-body"> | |
<div id="modalMenuContainer" class="menu-container"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Offcanvas --> | |
<div class="offcanvas offcanvas-end" id="menuOffcanvas" tabindex="-1"> | |
<div id="offcanvasHeader" class="menu-header"> | |
<button id="offcanvasBackButton" class="header-back"> | |
<i class="bi bi-chevron-left"></i> | |
</button> | |
<h5 id="offcanvasTitle" class="menu-title">Settings</h5> | |
<div id="offcanvasHeaderAction" class="header-action"></div> | |
<button id="offcanvasCloseButton" class="header-close"> | |
<i class="bi bi-x"></i> | |
</button> | |
</div> | |
<div class="offcanvas-body"> | |
<div id="offcanvasMenuContainer" class="menu-container"></div> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Unified menu configuration | |
const menuConfig = { | |
main: { | |
title: 'Main Menu', | |
action: '<span class="badge bg-primary badge-clickable">v2.0</span>', | |
items: [ | |
{ label: 'Profile', icon: 'bi-person', route: 'profile' }, | |
{ label: 'Settings', icon: 'bi-gear', route: 'settings' }, | |
{ label: 'Help', icon: 'bi-question-circle', route: 'help' }, | |
{ label: 'Forms', icon: 'bi-file-earmark-text', route: 'forms' } | |
], | |
footer: '<button class="btn btn-sm btn-outline-danger" onclick="handleAction(\'signout\')">Sign Out</button>' | |
}, | |
profile: { | |
title: 'Profile', | |
parent: 'main', | |
items: [ | |
{ label: 'Edit Profile', icon: 'bi-pencil', route: 'profile/edit' }, | |
{ label: 'Change Password', icon: 'bi-lock', route: 'profile/password' }, | |
{ label: 'Privacy Settings', icon: 'bi-shield-check', route: 'profile/privacy' } | |
] | |
}, | |
'profile/edit': { | |
title: 'Edit Profile', | |
parent: 'profile', | |
items: [ | |
{ label: 'Personal Info', icon: 'bi-person-lines-fill', route: 'profile-form' }, | |
{ label: 'Contact Details', icon: 'bi-envelope', route: 'contact-form' }, | |
{ label: 'Preferences', icon: 'bi-sliders', route: 'preferences-form' } | |
], | |
footer: '<button class="btn btn-sm btn-primary" onclick="handleAction(\'save-profile\')">Save Changes</button>' | |
}, | |
'profile/password': { | |
title: 'Change Password', | |
parent: 'profile', | |
type: 'form', | |
form: { | |
id: 'password-form', | |
sections: [ | |
{ | |
title: 'Password Change', | |
fields: [ | |
{ name: 'current_password', label: 'Current Password', type: 'password', required: true }, | |
{ name: 'new_password', label: 'New Password', type: 'password', required: true }, | |
{ name: 'confirm_password', label: 'Confirm Password', type: 'password', required: true } | |
] | |
} | |
], | |
actions: [ | |
{ label: 'Cancel', type: 'secondary', action: 'cancel' }, | |
{ label: 'Change Password', type: 'primary', action: 'save' } | |
] | |
} | |
}, | |
'profile/privacy': { | |
title: 'Privacy Settings', | |
parent: 'profile', | |
items: [ | |
{ label: 'Profile Visibility', icon: 'bi-eye', toggle: true, key: 'profile_visible' }, | |
{ label: 'Search Indexing', icon: 'bi-search', toggle: true, key: 'search_indexing' }, | |
{ label: 'Contact Sync', icon: 'bi-people', toggle: true, key: 'contact_sync' } | |
] | |
}, | |
settings: { | |
title: 'Settings', | |
parent: 'main', | |
action: '<i class="bi bi-three-dots" style="cursor: pointer;"></i>', | |
items: [ | |
{ label: 'Notifications', icon: 'bi-bell', route: 'settings/notifications' }, | |
{ label: 'Display', icon: 'bi-display', route: 'settings/display' }, | |
{ label: 'Advanced', icon: 'bi-gear-wide-connected', route: 'settings/advanced' } | |
] | |
}, | |
'settings/notifications': { | |
title: 'Notifications', | |
parent: 'settings', | |
items: [ | |
{ label: 'Push Notifications', icon: 'bi-app-indicator', toggle: true, key: 'push_notifications' }, | |
{ label: 'Email Alerts', icon: 'bi-envelope', toggle: true, key: 'email_alerts' }, | |
{ label: 'SMS Updates', icon: 'bi-chat-text', toggle: true, key: 'sms_updates' } | |
] | |
}, | |
'settings/display': { | |
title: 'Display Settings', | |
parent: 'settings', | |
items: [ | |
{ label: 'Dark Mode', icon: 'bi-moon-stars', toggle: true, key: 'dark_mode' }, | |
{ label: 'Large Text', icon: 'bi-fonts', toggle: true, key: 'large_text' }, | |
{ label: 'High Contrast', icon: 'bi-palette2', toggle: true, key: 'high_contrast' } | |
] | |
}, | |
'settings/advanced': { | |
title: 'Advanced Settings', | |
parent: 'settings', | |
action: '<span class="badge bg-warning badge-clickable">Beta</span>', | |
items: [ | |
{ label: 'Developer Mode', icon: 'bi-code-slash', toggle: true, key: 'developer_mode' }, | |
{ label: 'Debug Logs', icon: 'bi-bug', toggle: true, key: 'debug_logs' }, | |
{ label: 'Experimental Features', icon: 'bi-flask', toggle: true, key: 'experimental' } | |
] | |
}, | |
help: { | |
title: 'Help & Support', | |
parent: 'main', | |
items: [ | |
{ label: 'FAQ', icon: 'bi-question-circle', action: 'show-faq' }, | |
{ label: 'Contact Support', icon: 'bi-headset', action: 'contact-support' }, | |
{ label: 'User Guide', icon: 'bi-book', action: 'show-guide' }, | |
{ label: 'Report Bug', icon: 'bi-bug', action: 'report-bug' } | |
] | |
}, | |
forms: { | |
title: 'Form Examples', | |
parent: 'main', | |
items: [ | |
{ label: 'Profile Form', icon: 'bi-person-badge', route: 'profile-form' }, | |
{ label: 'Contact Form', icon: 'bi-envelope-at', route: 'contact-form' }, | |
{ label: 'Preferences', icon: 'bi-sliders', route: 'preferences-form' } | |
] | |
}, | |
'profile-form': { | |
title: 'Profile Information', | |
parent: 'forms', | |
type: 'form', | |
form: { | |
id: 'profile-form', | |
sections: [ | |
{ | |
title: 'Basic Information', | |
fields: [ | |
{ name: 'first_name', label: 'First Name', type: 'text', required: true, placeholder: 'Enter your first name' }, | |
{ name: 'last_name', label: 'Last Name', type: 'text', required: true, placeholder: 'Enter your last name' }, | |
{ name: 'email', label: 'Email', type: 'email', required: true, placeholder: '[email protected]' }, | |
{ name: 'phone', label: 'Phone Number', type: 'tel', placeholder: '+1 (555) 000-0000' } | |
] | |
}, | |
{ | |
title: 'Additional Details', | |
fields: [ | |
{ name: 'bio', label: 'Bio', type: 'textarea', rows: 4, placeholder: 'Tell us about yourself...' }, | |
{ name: 'website', label: 'Website', type: 'url', placeholder: 'https://yourwebsite.com' }, | |
{ name: 'location', label: 'Location', type: 'text', placeholder: 'City, Country' } | |
] | |
} | |
], | |
actions: [ | |
{ label: 'Cancel', type: 'secondary', action: 'cancel' }, | |
{ label: 'Save Profile', type: 'primary', action: 'save' } | |
] | |
} | |
}, | |
'contact-form': { | |
title: 'Contact Information', | |
parent: 'forms', | |
type: 'form', | |
form: { | |
id: 'contact-form', | |
sections: [ | |
{ | |
title: 'Contact Details', | |
fields: [ | |
{ name: 'company', label: 'Company', type: 'text', placeholder: 'Your company name' }, | |
{ name: 'job_title', label: 'Job Title', type: 'text', placeholder: 'Your role' }, | |
{ name: 'work_phone', label: 'Work Phone', type: 'tel', placeholder: '+1 (555) 000-0000' }, | |
{ name: 'work_email', label: 'Work Email', type: 'email', placeholder: '[email protected]' } | |
] | |
}, | |
{ | |
title: 'Preferences', | |
fields: [ | |
{ name: 'contact_method', label: 'Preferred Contact Method', type: 'select', options: [ | |
{ value: 'email', label: 'Email' }, | |
{ value: 'phone', label: 'Phone' }, | |
{ value: 'both', label: 'Both' } | |
]}, | |
{ name: 'newsletter', label: 'Subscribe to newsletter', type: 'checkbox' } | |
] | |
} | |
], | |
actions: [ | |
{ label: 'Cancel', type: 'secondary', action: 'cancel' }, | |
{ label: 'Save Contact Info', type: 'success', action: 'save' } | |
] | |
} | |
}, | |
'preferences-form': { | |
title: 'User Preferences', | |
parent: 'forms', | |
type: 'form', | |
form: { | |
id: 'preferences-form', | |
sections: [ | |
{ | |
title: 'Display Preferences', | |
fields: [ | |
{ name: 'theme', label: 'Theme', type: 'select', options: [ | |
{ value: 'light', label: 'Light' }, | |
{ value: 'dark', label: 'Dark' }, | |
{ value: 'auto', label: 'Auto' } | |
]}, | |
{ name: 'language', label: 'Language', type: 'select', options: [ | |
{ value: 'en', label: 'English' }, | |
{ value: 'es', label: 'Español' }, | |
{ value: 'fr', label: 'Français' } | |
]}, | |
{ name: 'timezone', label: 'Timezone', type: 'select', options: [ | |
{ value: 'UTC', label: 'UTC' }, | |
{ value: 'America/New_York', label: 'Eastern Time' }, | |
{ value: 'America/Chicago', label: 'Central Time' }, | |
{ value: 'America/Los_Angeles', label: 'Pacific Time' } | |
]} | |
] | |
}, | |
{ | |
title: 'Notification Preferences', | |
fields: [ | |
{ name: 'email_notifications', label: 'Email notifications', type: 'checkbox' }, | |
{ name: 'push_notifications', label: 'Push notifications', type: 'checkbox' }, | |
{ name: 'marketing_emails', label: 'Marketing emails', type: 'checkbox' } | |
] | |
} | |
], | |
actions: [ | |
{ label: 'Reset to Defaults', type: 'outline-secondary', action: 'reset' }, | |
{ label: 'Cancel', type: 'secondary', action: 'cancel' }, | |
{ label: 'Save Preferences', type: 'primary', action: 'save' } | |
] | |
} | |
} | |
}; | |
// State management with persistence | |
class StateManager { | |
constructor() { | |
this.state = this.loadState(); | |
this.formData = this.loadFormData(); | |
} | |
loadState() { | |
try { | |
return JSON.parse(localStorage.getItem('menuState')) || {}; | |
} catch { | |
return {}; | |
} | |
} | |
saveState() { | |
try { | |
localStorage.setItem('menuState', JSON.stringify(this.state)); | |
} catch (e) { | |
console.warn('Could not save state to localStorage:', e); | |
} | |
} | |
loadFormData() { | |
try { | |
return JSON.parse(localStorage.getItem('formData')) || {}; | |
} catch { | |
return {}; | |
} | |
} | |
saveFormData() { | |
try { | |
localStorage.setItem('formData', JSON.stringify(this.formData)); | |
this.updateFormDataDisplay(); | |
} catch (e) { | |
console.warn('Could not save form data to localStorage:', e); | |
} | |
} | |
updateFormDataDisplay() { | |
const display = document.getElementById('formDataDisplay'); | |
const keys = Object.keys(this.formData); | |
if (keys.length === 0) { | |
display.textContent = 'No form data saved'; | |
display.className = 'text-muted'; | |
} else { | |
display.textContent = `${keys.length} form(s) saved: ${keys.join(', ')}`; | |
display.className = 'text-success'; | |
} | |
} | |
get(key) { | |
return this.state[key] || false; | |
} | |
set(key, value) { | |
this.state[key] = value; | |
this.saveState(); | |
} | |
toggle(key) { | |
this.set(key, !this.get(key)); | |
return this.get(key); | |
} | |
getFormData(formId) { | |
return this.formData[formId] || {}; | |
} | |
setFormData(formId, data) { | |
this.formData[formId] = { ...this.formData[formId], ...data }; | |
this.saveFormData(); | |
} | |
resetFormData(formId) { | |
delete this.formData[formId]; | |
this.saveFormData(); | |
} | |
} | |
// Menu renderer with unified support for both modals and offcanvas | |
class MenuRenderer { | |
constructor(containerId, type) { | |
this.container = document.getElementById(containerId); | |
this.type = type; | |
this.currentPath = ''; | |
// Get references to header elements | |
this.titleElement = document.getElementById(`${type}Title`); | |
this.backButton = document.getElementById(`${type}BackButton`); | |
this.headerAction = document.getElementById(`${type}HeaderAction`); | |
this.closeButton = document.getElementById(`${type}CloseButton`); | |
// Set up event handlers | |
this.backButton.onclick = () => router.goBack(); | |
this.closeButton.onclick = () => router.close(); | |
} | |
render(path) { | |
const menu = menuConfig[path]; | |
if (!menu || this.currentPath === path) return; | |
const isForward = this.isForwardNavigation(path); | |
// Update header first | |
this.updateHeader(menu); | |
// Create and render content | |
const level = menu.type === 'form' ? | |
this.createFormLevel(path, menu) : | |
this.createMenuLevel(path, menu); | |
this.container.appendChild(level); | |
this.animate(level, isForward); | |
this.currentPath = path; | |
} | |
updateHeader(menu) { | |
// Update title | |
this.titleElement.textContent = menu.title; | |
// Show/hide back button with animation | |
if (menu.parent) { | |
this.backButton.classList.add('visible'); | |
} else { | |
this.backButton.classList.remove('visible'); | |
} | |
// Update action area | |
if (menu.action) { | |
this.headerAction.innerHTML = menu.action; | |
this.setupActionHandlers(this.headerAction, menu); | |
} else { | |
this.headerAction.innerHTML = ''; | |
} | |
} | |
setupActionHandlers(container, menu) { | |
container.querySelectorAll('.badge-clickable, i[style*="cursor"]').forEach(el => { | |
el.onclick = (e) => { | |
e.stopPropagation(); | |
handleAction(`header-action-${menu.title.toLowerCase().replace(/\s+/g, '-')}`); | |
}; | |
}); | |
} | |
isForwardNavigation(newPath) { | |
if (!this.currentPath) return true; | |
return newPath.length > this.currentPath.length || | |
newPath.includes(this.currentPath); | |
} | |
createMenuLevel(path, menu) { | |
const level = document.createElement('div'); | |
level.className = 'menu-level'; | |
// Items container | |
const itemsContainer = document.createElement('div'); | |
itemsContainer.style.cssText = ` | |
height: ${menu.footer ? 'calc(100% - 60px)' : '100%'}; | |
overflow-y: auto; | |
`; | |
menu.items.forEach(item => { | |
itemsContainer.appendChild(this.createMenuItem(path, item)); | |
}); | |
level.appendChild(itemsContainer); | |
// Footer | |
if (menu.footer) { | |
const footer = document.createElement('div'); | |
footer.className = 'menu-footer'; | |
footer.innerHTML = menu.footer; | |
level.appendChild(footer); | |
} | |
return level; | |
} | |
createFormLevel(path, menu) { | |
const level = document.createElement('div'); | |
level.className = 'menu-level'; | |
const form = document.createElement('form'); | |
form.className = 'menu-form'; | |
form.onsubmit = (e) => { | |
e.preventDefault(); | |
this.handleFormSubmit(menu.form.id, form); | |
}; | |
// Create form sections | |
menu.form.sections.forEach(section => { | |
const sectionEl = this.createFormSection(section, menu.form.id); | |
form.appendChild(sectionEl); | |
}); | |
// Create form actions | |
if (menu.form.actions) { | |
const actionsEl = this.createFormActions(menu.form.actions, menu.form.id); | |
form.appendChild(actionsEl); | |
} | |
level.appendChild(form); | |
// Load saved form data | |
setTimeout(() => this.loadFormData(menu.form.id, form), 0); | |
return level; | |
} | |
createMenuItem(path, item) { | |
const itemEl = document.createElement('div'); | |
itemEl.className = 'menu-item'; | |
const leftContent = document.createElement('div'); | |
leftContent.className = 'd-flex align-items-center'; | |
leftContent.innerHTML = ` | |
<i class="${item.icon} me-3"></i> | |
<span>${item.label}</span> | |
`; | |
const rightContent = document.createElement('div'); | |
rightContent.innerHTML = this.getItemAction(item); | |
itemEl.appendChild(leftContent); | |
itemEl.appendChild(rightContent); | |
// Set up click handler | |
if (item.route) { | |
itemEl.onclick = () => router.navigate(`#${this.type}/${item.route}`); | |
} else if (item.action) { | |
itemEl.onclick = () => handleAction(item.action); | |
} else if (item.toggle) { | |
const toggle = rightContent.querySelector('input[type="checkbox"]'); | |
if (toggle) { | |
toggle.onchange = () => { | |
const newState = stateManager.toggle(item.key); | |
this.showToast(`${item.label} ${newState ? 'enabled' : 'disabled'}`); | |
}; | |
} | |
} | |
return itemEl; | |
} | |
getItemAction(item) { | |
if (item.toggle) { | |
const checked = stateManager.get(item.key); | |
return ` | |
<div class="form-check form-switch"> | |
<input class="form-check-input" type="checkbox" ${checked ? 'checked' : ''}> | |
</div> | |
`; | |
} | |
return item.route ? '<i class="bi bi-chevron-right text-muted"></i>' : ''; | |
} | |
createFormSection(section, formId) { | |
const sectionEl = document.createElement('div'); | |
sectionEl.className = 'form-section'; | |
if (section.title) { | |
const titleEl = document.createElement('h6'); | |
titleEl.textContent = section.title; | |
sectionEl.appendChild(titleEl); | |
} | |
section.fields.forEach(field => { | |
const fieldEl = this.createFormField(field, formId); | |
sectionEl.appendChild(fieldEl); | |
}); | |
return sectionEl; | |
} | |
createFormField(field, formId) { | |
const groupEl = document.createElement('div'); | |
groupEl.className = 'form-group'; | |
const labelEl = document.createElement('label'); | |
labelEl.className = 'form-label'; | |
labelEl.textContent = field.label; | |
labelEl.htmlFor = `${formId}-${field.name}`; | |
groupEl.appendChild(labelEl); | |
let inputEl; | |
switch (field.type) { | |
case 'textarea': | |
inputEl = document.createElement('textarea'); | |
inputEl.rows = field.rows || 3; | |
break; | |
case 'select': | |
inputEl = document.createElement('select'); | |
field.options.forEach(option => { | |
const optionEl = document.createElement('option'); | |
optionEl.value = option.value; | |
optionEl.textContent = option.label; | |
inputEl.appendChild(optionEl); | |
}); | |
break; | |
case 'checkbox': | |
inputEl = document.createElement('input'); | |
inputEl.type = 'checkbox'; | |
inputEl.className = 'form-check-input'; | |
const checkGroupEl = document.createElement('div'); | |
checkGroupEl.className = 'form-check'; | |
checkGroupEl.appendChild(inputEl); | |
const checkLabelEl = document.createElement('label'); | |
checkLabelEl.className = 'form-check-label'; | |
checkLabelEl.textContent = field.label; | |
checkLabelEl.htmlFor = `${formId}-${field.name}`; | |
checkGroupEl.appendChild(checkLabelEl); | |
return checkGroupEl; | |
default: | |
inputEl = document.createElement('input'); | |
inputEl.type = field.type || 'text'; | |
} | |
inputEl.id = `${formId}-${field.name}`; | |
inputEl.name = field.name; | |
inputEl.className += field.type === 'select' ? ' form-select' : ' form-control'; | |
if (field.placeholder) inputEl.placeholder = field.placeholder; | |
if (field.required) inputEl.required = true; | |
// Auto-save on input | |
inputEl.addEventListener('input', () => { | |
this.autoSaveForm(formId); | |
}); | |
groupEl.appendChild(inputEl); | |
return groupEl; | |
} | |
createFormActions(actions, formId) { | |
const actionsEl = document.createElement('div'); | |
actionsEl.className = 'form-actions d-flex gap-2 justify-content-end'; | |
actions.forEach(action => { | |
const btnEl = document.createElement('button'); | |
btnEl.type = action.action === 'save' ? 'submit' : 'button'; | |
btnEl.className = `btn btn-${action.type}`; | |
btnEl.textContent = action.label; | |
if (action.action === 'cancel') { | |
btnEl.onclick = () => router.goBack(); | |
} else if (action.action === 'reset') { | |
btnEl.onclick = () => this.resetForm(formId); | |
} | |
actionsEl.appendChild(btnEl); | |
}); | |
return actionsEl; | |
} | |
loadFormData(formId, form) { | |
const savedData = stateManager.getFormData(formId); | |
const formData = new FormData(form); | |
Object.entries(savedData).forEach(([key, value]) => { | |
const input = form.querySelector(`[name="${key}"]`); | |
if (input) { | |
if (input.type === 'checkbox') { | |
input.checked = value; | |
} else { | |
input.value = value; | |
} | |
} | |
}); | |
} | |
autoSaveForm(formId) { | |
const form = document.querySelector(`form`); | |
if (!form) return; | |
const formData = new FormData(form); | |
const data = {}; | |
for (const [key, value] of formData.entries()) { | |
const input = form.querySelector(`[name="${key}"]`); | |
if (input && input.type === 'checkbox') { | |
data[key] = input.checked; | |
} else { | |
data[key] = value; | |
} | |
} | |
stateManager.setFormData(formId, data); | |
} | |
handleFormSubmit(formId, form) { | |
const submitBtn = form.querySelector('button[type="submit"]'); | |
const originalText = submitBtn.textContent; | |
// Show loading state | |
submitBtn.classList.add('btn-loading'); | |
submitBtn.textContent = ''; | |
submitBtn.disabled = true; | |
// Simulate save process | |
setTimeout(() => { | |
this.autoSaveForm(formId); | |
this.showToast(`${formId.replace('-', ' ')} saved successfully!`, 'success'); | |
// Reset button state | |
submitBtn.classList.remove('btn-loading'); | |
submitBtn.textContent = originalText; | |
submitBtn.disabled = false; | |
// Navigate back after successful save | |
setTimeout(() => router.goBack(), 1000); | |
}, 1500); | |
} | |
resetForm(formId) { | |
if (confirm('Are you sure you want to reset all fields to their default values?')) { | |
stateManager.resetFormData(formId); | |
const form = document.querySelector('form'); | |
if (form) { | |
form.reset(); | |
this.showToast('Form reset to defaults', 'info'); | |
} | |
} | |
} | |
animate(newLevel, isForward) { | |
const currentLevel = this.container.querySelector('.menu-level.active'); | |
if (!currentLevel) { | |
newLevel.classList.add('active'); | |
return; | |
} | |
// Set initial position | |
newLevel.style.transform = isForward ? 'translateX(100%)' : 'translateX(-100%)'; | |
requestAnimationFrame(() => { | |
requestAnimationFrame(() => { | |
currentLevel.style.transform = isForward ? 'translateX(-100%)' : 'translateX(100%)'; | |
newLevel.style.transform = 'translateX(0)'; | |
newLevel.classList.add('active'); | |
currentLevel.classList.remove('active'); | |
}); | |
}); | |
// Clean up after animation | |
setTimeout(() => { | |
if (currentLevel && currentLevel.parentNode) { | |
currentLevel.remove(); | |
} | |
newLevel.style.transform = ''; | |
}, 350); | |
} | |
showToast(message, type = 'info') { | |
// Create toast notification | |
const toastContainer = document.getElementById('toastContainer') || this.createToastContainer(); | |
const toast = document.createElement('div'); | |
toast.className = `toast align-items-center text-bg-${type} border-0`; | |
toast.setAttribute('role', 'alert'); | |
toast.innerHTML = ` | |
<div class="d-flex"> | |
<div class="toast-body">${message}</div> | |
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | |
</div> | |
`; | |
toastContainer.appendChild(toast); | |
const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); | |
bsToast.show(); | |
// Remove toast element after it's hidden | |
toast.addEventListener('hidden.bs.toast', () => { | |
toast.remove(); | |
}); | |
} | |
createToastContainer() { | |
const container = document.createElement('div'); | |
container.id = 'toastContainer'; | |
container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; | |
container.style.zIndex = '9999'; | |
document.body.appendChild(container); | |
return container; | |
} | |
clear() { | |
this.container.innerHTML = ''; | |
this.currentPath = ''; | |
this.titleElement.textContent = this.type === 'modal' ? 'Menu' : 'Settings'; | |
this.backButton.classList.remove('visible'); | |
this.headerAction.innerHTML = ''; | |
} | |
} | |
// Enhanced Router | |
class Router { | |
constructor() { | |
this.modal = new bootstrap.Modal(document.getElementById('menuModal')); | |
this.offcanvas = new bootstrap.Offcanvas(document.getElementById('menuOffcanvas')); | |
this.modalRenderer = new MenuRenderer('modalMenuContainer', 'modal'); | |
this.offcanvasRenderer = new MenuRenderer('offcanvasMenuContainer', 'offcanvas'); | |
window.addEventListener('hashchange', () => this.handleRoute()); | |
window.addEventListener('popstate', () => this.handleRoute()); | |
// Close handlers | |
document.getElementById('menuModal').addEventListener('hidden.bs.modal', () => { | |
if (location.hash.startsWith('#modal')) { | |
history.replaceState(null, '', window.location.pathname); | |
} | |
}); | |
document.getElementById('menuOffcanvas').addEventListener('hidden.bs.offcanvas', () => { | |
if (location.hash.startsWith('#offcanvas')) { | |
history.replaceState(null, '', window.location.pathname); | |
} | |
}); | |
this.handleRoute(); | |
} | |
navigate(hash) { | |
if (location.hash === hash) return; | |
location.hash = hash; | |
} | |
goBack() { | |
const [type, ...pathParts] = location.hash.slice(1).split('/'); | |
const currentPath = pathParts.join('/'); | |
const menu = menuConfig[currentPath]; | |
if (menu?.parent) { | |
this.navigate(`#${type}/${menu.parent}`); | |
} else { | |
this.close(); | |
} | |
} | |
close() { | |
history.replaceState(null, '', window.location.pathname); | |
} | |
handleRoute() { | |
const hash = location.hash.slice(1); | |
document.getElementById('currentRoute').textContent = location.hash || '#'; | |
if (!hash) { | |
this.modal.hide(); | |
this.offcanvas.hide(); | |
this.modalRenderer.clear(); | |
this.offcanvasRenderer.clear(); | |
return; | |
} | |
const [type, ...pathParts] = hash.split('/'); | |
const path = pathParts.join('/'); | |
if (type === 'modal' && menuConfig[path]) { | |
this.offcanvas.hide(); | |
this.modal.show(); | |
this.modalRenderer.render(path); | |
} else if (type === 'offcanvas' && menuConfig[path]) { | |
this.modal.hide(); | |
this.offcanvas.show(); | |
this.offcanvasRenderer.render(path); | |
} else { | |
this.close(); | |
} | |
} | |
} | |
// Global action handler | |
function handleAction(action) { | |
const actions = { | |
'signout': () => { | |
if (confirm('Are you sure you want to sign out?')) { | |
alert('Signed out successfully!'); | |
router.close(); | |
} | |
}, | |
'save-profile': () => { | |
alert('Profile changes saved!'); | |
}, | |
'show-faq': () => { | |
alert('Opening FAQ...'); | |
}, | |
'contact-support': () => { | |
alert('Opening support contact form...'); | |
}, | |
'show-guide': () => { | |
alert('Opening user guide...'); | |
}, | |
'report-bug': () => { | |
alert('Opening bug report form...'); | |
}, | |
'header-action-main-menu': () => { | |
alert('Version info: Enhanced Menu System v2.0'); | |
}, | |
'header-action-settings': () => { | |
alert('Settings menu options'); | |
}, | |
'header-action-advanced-settings': () => { | |
alert('Beta features available!'); | |
} | |
}; | |
if (actions[action]) { | |
actions[action](); | |
} else { | |
console.log('Action triggered:', action); | |
} | |
} | |
// Initialize | |
const stateManager = new StateManager(); | |
const router = new Router(); | |
// Make handleAction global | |
window.handleAction = handleAction; | |
// Update form data display on load | |
stateManager.updateFormDataDisplay(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment