Skip to content

Instantly share code, notes, and snippets.

@zilveer
Last active July 20, 2025 19:01
Show Gist options
  • Save zilveer/8279ef639393cf49ac7f85e9da052ecc to your computer and use it in GitHub Desktop.
Save zilveer/8279ef639393cf49ac7f85e9da052ecc to your computer and use it in GitHub Desktop.
Modal offvanvas router
<!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