Created
July 20, 2025 21:02
-
-
Save zilveer/ebe5175dda554120b44a334299843fd7 to your computer and use it in GitHub Desktop.
Modal offvanvas router 1.58
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>Advanced Routable Bootstrap Menus (Font Awesome)</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/font-awesome/6.5.2/css/all.min.css" rel="stylesheet"> | |
<style> | |
:root { | |
--primary-color: #007bff; | |
--primary-hover-color: #0056b3; | |
--light-blue-hover: #e9f5ff; | |
--text-color: #333; | |
--secondary-text-color: #6c757d; | |
--border-color: #e9ecef; | |
--header-bg-color: #f8f9fa; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4f7f6; | |
color: var(--text-color); | |
} | |
.container { | |
background-color: #ffffff; | |
padding: 30px; | |
border-radius: 8px; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); | |
margin-top: 50px; | |
} | |
h2 { | |
color: var(--primary-color); | |
margin-bottom: 25px; | |
font-weight: 600; | |
} | |
.menu-container { | |
position: relative; | |
overflow: hidden; /* Important for containing sliding menu levels */ | |
width: 100%; | |
height: 100%; | |
} | |
.menu-level { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
transition: transform 0.3s ease-in-out; | |
background: white; | |
opacity: 1; | |
box-sizing: border-box; | |
padding-bottom: 60px; /* Space for footer */ | |
} | |
.menu-level.slide-right { | |
transform: translateX(100%); | |
} | |
.menu-level.slide-left { | |
transform: translateX(-100%); | |
} | |
.menu-level.active { | |
transform: translateX(0); | |
opacity: 1; | |
} | |
.menu-item { | |
padding: 0.9rem 1.2rem; | |
border-bottom: 1px solid var(--border-color); | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
transition: background-color 0.2s, transform 0.1s ease-out; | |
font-size: 1.05rem; | |
color: #495057; | |
} | |
.menu-item:hover { | |
background: var(--light-blue-hover); | |
transform: translateX(3px); | |
} | |
.menu-item.disabled { | |
opacity: 0.5; | |
cursor: not-allowed; | |
background: #f8f9fa; | |
transform: none; | |
} | |
.menu-item.disabled:hover { | |
background: #f8f9fa; | |
} | |
.menu-item i { | |
margin-right: 12px; | |
color: var(--primary-color); | |
min-width: 20px; | |
text-align: center; | |
} | |
.menu-item-content { | |
flex-grow: 1; | |
} | |
.menu-item-description { | |
font-size: 0.85rem; | |
color: var(--secondary-text-color); | |
margin-top: 2px; | |
} | |
.menu-footer { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
border-top: 1px solid #dee2e6; | |
padding: 0.8rem 1rem; | |
background: var(--header-bg-color); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 60px; | |
} | |
/* Adjust modal/offcanvas heights for better display */ | |
.modal-content { | |
height: 550px; /* Fixed height for demo */ | |
display: flex; | |
flex-direction: column; | |
} | |
/* Make offcanvas-body take full height and be scrollable if needed */ | |
.offcanvas-body { | |
flex-grow: 1; | |
padding: 0; | |
position: relative; /* Crucial for absolute positioning of .menu-level children */ | |
overflow: hidden; /* Hide overflow from sliding menus inside */ | |
} | |
.modal-body { | |
flex-grow: 1; | |
padding: 0; | |
position: relative; /* Crucial for absolute positioning of .menu-level children */ | |
overflow: hidden; /* Hide overflow from sliding menus inside */ | |
} | |
.modal-header, .offcanvas-header { | |
position: sticky; /* Make header sticky when content scrolls */ | |
top: 0; | |
z-index: 10; /* Ensure it's above scrolling content */ | |
min-height: 65px; | |
border-bottom: 1px solid #dee2e6; | |
padding: 1rem; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background-color: var(--header-bg-color); | |
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | |
flex-shrink: 0; /* Prevent header from shrinking */ | |
} | |
.modal-header h5, .offcanvas-header h5 { | |
margin: 0; | |
font-weight: 700; | |
color: #343a40; | |
font-size: 1.25rem; | |
} | |
.header-back { | |
background: none; | |
border: none; | |
color: var(--primary-color); | |
cursor: pointer; | |
padding: 0.25rem; | |
font-size: 1.25rem; | |
position: absolute; | |
left: 1.2rem; | |
top: 50%; | |
transform: translateY(-50%); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: color 0.2s; | |
} | |
.header-back:hover { | |
color: var(--primary-hover-color); | |
} | |
.header-action { | |
position: absolute; | |
right: 3.5rem; | |
top: 50%; | |
transform: translateY(-50%); | |
display: flex; | |
align-items: center; | |
color: var(--secondary-text-color); | |
cursor: default; /* Default cursor for action area unless inner elements are clickable */ | |
} | |
.header-action > * { /* Make elements inside header-action clickable if they have a click handler */ | |
cursor: pointer; | |
} | |
.header-action .badge { | |
font-size: 0.8rem; | |
padding: 0.4em 0.7em; | |
} | |
.btn-close { | |
position: absolute !important; | |
top: 50% !important; | |
right: 1.2rem !important; | |
transform: translateY(-50%) !important; | |
z-index: 1070 !important; | |
font-size: 0.9rem; | |
} | |
.info-content { | |
padding: 1.5rem; | |
line-height: 1.7; | |
height: 100%; | |
overflow-y: auto; /* Allow info content to scroll */ | |
color: #343a40; | |
} | |
.info-content h3, .info-content h5 { | |
color: var(--primary-color); | |
margin-top: 1.5rem; | |
margin-bottom: 0.8rem; | |
font-weight: 600; | |
} | |
.info-content ul { | |
padding-left: 20px; | |
margin-bottom: 1rem; | |
} | |
.info-content ul li { | |
margin-bottom: 0.5rem; | |
} | |
.info-content a { | |
color: var(--primary-color); | |
text-decoration: none; | |
} | |
.info-content a:hover { | |
text-decoration: underline; | |
} | |
.state-indicator { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
z-index: 1050; | |
background: rgba(0, 123, 255, 0.15); | |
border: 1px solid var(--primary-color); | |
border-radius: 6px; | |
padding: 0.7rem 1.2rem; | |
font-size: 0.9rem; | |
max-width: 350px; | |
color: var(--primary-hover-color); | |
box-shadow: 0 2px 10px rgba(0, 123, 255, 0.2); | |
opacity: 0; | |
transform: translateY(-20px); | |
transition: opacity 0.3s ease-out, transform 0.3s ease-out, background-color 0.2s, border-color 0.2s; | |
} | |
.state-indicator.show { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
.state-indicator.alert-danger { | |
background-color: rgba(220, 53, 69, 0.15); | |
border-color: #dc3545; | |
color: #dc3545; | |
} | |
.state-indicator.alert-info { | |
background-color: rgba(13, 202, 240, 0.15); | |
border-color: #0dcaf0; | |
color: #087990; | |
} | |
.form-check.form-switch { | |
display: flex; | |
align-items: center; | |
justify-content: flex-end; | |
margin-left: auto; | |
} | |
.form-check-input { | |
margin-left: 0 !important; | |
cursor: pointer; | |
} | |
.profile-edit-form { | |
padding: 1.5rem; | |
height: 100%; | |
overflow-y: auto; /* Allow form content to scroll */ | |
} | |
.profile-edit-form .form-label { | |
font-weight: 500; | |
color: #343a40; | |
margin-bottom: 0.3rem; | |
} | |
.profile-edit-form .form-control { | |
margin-bottom: 1rem; | |
} | |
.profile-edit-form .btn-group { | |
margin-top: 1.5rem; | |
display: flex; | |
gap: 0.5rem; | |
justify-content: flex-end; | |
} | |
.menu-group-title { | |
padding: 0.5rem 1.2rem; | |
font-size: 0.85rem; | |
font-weight: 600; | |
color: var(--secondary-text-color); | |
background-color: var(--header-bg-color); | |
border-bottom: 1px solid var(--border-color); | |
margin-top: 0.5rem; | |
text-transform: uppercase; | |
} | |
.menu-group-title:first-child { | |
margin-top: 0; | |
} | |
/* --- Modal Positioning Overrides --- */ | |
/* Bootstrap 5 provides .modal-dialog-centered and .modal-fullscreen. We add top/bottom */ | |
.modal.modal-top .modal-dialog { | |
top: 0; | |
margin-top: 20px; /* Small margin from top */ | |
transform: translate(0, 0) !important; /* Override default Bootstrap center transform */ | |
} | |
.modal.modal-bottom .modal-dialog { | |
bottom: 0; | |
margin-bottom: 20px; /* Small margin from bottom */ | |
top: auto; /* Ensure it's not trying to position from top */ | |
transform: translate(0, 0) !important; /* Override default Bootstrap center transform */ | |
} | |
/* Ensure fade animation still works with custom positioning for modal-dialog */ | |
.modal.fade .modal-dialog { | |
transition: transform .3s ease-out, opacity .3s ease-out; /* Keep fade/scale transitions */ | |
} | |
/* No specific 'slide-right' or 'slide-left' for the .modal-dialog itself, rely on Bootstrap's default */ | |
/* Ensure .modal-fullscreen takes full height */ | |
.modal-fullscreen .modal-content { | |
height: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container mt-4"> | |
<h2>Advanced Routable Bootstrap Menus</h2> | |
<p class="lead text-muted">Demonstrates nested menus within Modals and Offcanvas components using a custom router and state management.</p> | |
<div class="d-flex gap-3 mb-4 flex-wrap"> | |
<button class="btn btn-primary" onclick="Router.navigate('modal/profile')">Profile Modal (Default)</button> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings')">Settings Offcanvas (Default)</button> | |
<button class="btn btn-info text-white" onclick="Router.navigate('modal/about')">About Us Modal (Default)</button> | |
<button class="btn btn-success" onclick="Router.navigate('offcanvas/account')">Account Offcanvas (Default)</button> | |
<hr class="w-100 my-2"> <h5>Modal Position Examples:</h5> | |
<button class="btn btn-primary" onclick="Router.navigate('modal/profile', {position: 'top'})">Modal: Top</button> | |
<button class="btn btn-primary" onclick="Router.navigate('modal/profile', {position: 'bottom'})">Modal: Bottom</button> | |
<button class="btn btn-primary" onclick="Router.navigate('modal/profile', {position: 'center'})">Modal: Center</button> | |
<button class="btn btn-primary" onclick="Router.navigate('modal/profile', {position: 'fullscreen'})">Modal: Fullscreen</button> | |
<h5>Offcanvas Position Examples:</h5> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings', {position: 'top'})">Offcanvas: Top</button> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings', {position: 'bottom'})">Offcanvas: Bottom</button> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings', {position: 'start'})">Offcanvas: Left</button> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings', {position: 'end'})">Offcanvas: Right</button> | |
<button class="btn btn-secondary" onclick="Router.navigate('offcanvas/settings', {position: 'fullscreen'})">Offcanvas: Fullscreen</button> | |
<hr class="w-100 my-2"> <h5>Direct Submenu & Position Examples:</h5> | |
<button class="btn btn-info" onclick="Router.navigate('modal/profile/edit', {position: 'top'})">Modal: Edit Profile (Top)</button> | |
<button class="btn btn-success" onclick="Router.navigate('offcanvas/settings/privacy', {position: 'bottom'})">Offcanvas: Privacy (Bottom)</button> | |
<button class="btn btn-warning text-white" onclick="Router.navigate('modal/profile/security')">Modal: Security (Default)</button> | |
<button class="btn btn-dark" onclick="Router.navigate('offcanvas/account/security-offcanvas', {position: 'start'})">Offcanvas: Account Security (Left)</button> | |
</div> | |
<div class="mb-4 d-flex align-items-center"> | |
<button class="btn btn-outline-primary btn-sm me-2" onclick="saveAppState()">Save State</button> | |
<button class="btn btn-outline-secondary btn-sm me-2" onclick="loadAppState()">Load State</button> | |
<button class="btn btn-outline-danger btn-sm" onclick="clearAppState()">Clear State</button> | |
<span class="ms-auto text-muted">Current route: <span id="currentRoute" class="fw-bold text-primary">#</span></span> | |
</div> | |
<p class="text-muted small">Click a button to open a menu. Navigate inside the menu. The current route will update accordingly. You can also save/load/clear the application state. New buttons demonstrate explicit modal/offcanvas positioning and direct submenu access.</p> | |
</div> | |
<div id="stateIndicator" class="state-indicator" role="status" aria-live="polite" style="display: none;"></div> | |
<div class="modal fade" id="menuModal" tabindex="-1" aria-labelledby="menuModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<button id="modalBackButton" class="header-back" style="display: none;" aria-label="Go Back"> | |
<i class="fa-solid fa-chevron-left"></i> | |
</button> | |
<h5 class="modal-title" id="menuModalLabel">Menu</h5> | |
<div id="modalHeaderAction" class="header-action"></div> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<div id="modalMenuContainer" class="menu-container"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="offcanvas" id="menuOffcanvas" tabindex="-1" aria-labelledby="menuOffcanvasLabel"> | |
<div class="offcanvas-header"> | |
<button id="offcanvasBackButton" class="header-back" style="display: none;" aria-label="Go Back"> | |
<i class="fa-solid fa-chevron-left"></i> | |
</button> | |
<h5 class="offcanvas-title" id="menuOffcanvasLabel">Menu</h5> | |
<div id="offcanvasHeaderAction" class="header-action"></div> | |
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></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 src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/js/all.min.js" defer></script> | |
<script> | |
// Router Library (embedded) | |
(function (root, factory) { | |
if (typeof define === 'function' && define.amd) { | |
define([], factory(root)); | |
} else if (typeof exports === 'object') { | |
module.exports = factory(root); | |
} else { | |
root.Router = factory(root); | |
} | |
})(typeof global !== "undefined" ? global : this.window || this.global, function (root) { | |
'use strict'; | |
const HASH_PREFIX = '^#'; | |
const SLASH_SUFFIX = /\/$/; | |
const internal = { | |
current: null, | |
routes: [], | |
history: [], | |
run_before: null, | |
run_after: null, | |
}; | |
const normalizeHash = (hash) => hash.replace(new RegExp(HASH_PREFIX), '').replace(SLASH_SUFFIX, ''); | |
const isFunction = (fn) => typeof fn === 'function'; | |
const runHook = (hook, route) => isFunction(hook) && hook.call(null, route); | |
const router = { | |
getFragment: () => normalizeHash(window.location.hash), | |
parseRoute: (route) => { | |
const paramNames = []; | |
let regexString = route.replace(/:([a-zA-Z0-9_]+)(\*)?/g, (match, p1, p2) => { | |
paramNames.push(p1); | |
if (p2 === '*') { | |
return '(.*?)'; | |
} | |
return '([^/]+)'; | |
}); | |
regexString = `^${regexString}$`; | |
return { regex: new RegExp(regexString), paramNames }; | |
}, | |
add: (route, handler, run_before, run_after) => { | |
const { regex, paramNames } = router.parseRoute(route); | |
internal.routes.push({ originalRoute: route, handler, route: regex, paramNames, run_before, run_after }); | |
return router; | |
}, | |
apply: (frg) => { | |
const fragment = frg || router.getFragment(); | |
if (internal.current === fragment && !frg) return router; // Do not re-apply if fragment is the same and not explicitly forced | |
for (const routeObj of internal.routes) { | |
const matches = fragment.match(routeObj.route); | |
if (matches) { | |
matches.shift(); | |
const params = {}; | |
routeObj.paramNames.forEach((name, i) => { | |
let value = matches[i]; | |
if (routeObj.originalRoute.includes(`:${name}*`) && typeof value === 'string') { | |
params[name] = value.split('/').filter(s => s !== ''); | |
} else { | |
params[name] = value; | |
} | |
}); | |
internal.current = fragment; | |
if (internal.history[internal.history.length - 1] !== fragment) { | |
internal.history.push(fragment); | |
} | |
runHook(internal.run_before, routeObj); | |
routeObj.handler.call({}, matches, params, routeObj.originalRoute); | |
runHook(internal.run_after, routeObj); | |
return router; | |
} | |
} | |
internal.current = null; | |
return router; | |
}, | |
start: () => { | |
const handleHashChange = () => { | |
const fragment = router.getFragment(); | |
if (internal.current !== fragment) { | |
router.apply(fragment); | |
} | |
}; | |
window.addEventListener('hashchange', handleHashChange); | |
if (!internal.current) router.apply(); | |
return router; | |
}, | |
// MODIFIED: router.navigate now accepts an options object | |
navigate: (path, options = {}) => { | |
document.title = options.title || document.title; | |
// Temporarily store options for the route handler to pick up. | |
// A more robust solution for complex apps might involve a global event bus | |
// or storing options in a dedicated state object. For this demo, this is sufficient. | |
router.__currentNavigationOptions = options; | |
window.location.hash = path ? `#${path.replace(/##/g, '#')}` : ''; | |
return router; | |
}, | |
back: () => { | |
if (internal.history.length > 0 && internal.history[internal.history.length - 1] === router.getFragment()) { | |
internal.history.pop(); | |
} | |
const previousPath = internal.history.pop(); | |
if (previousPath !== undefined) { | |
window.location.hash = previousPath ? `#${previousPath}` : ''; | |
} else { | |
window.location.hash = ''; | |
} | |
// When going back, reset any temporary navigation options | |
router.__currentNavigationOptions = {}; | |
return router; | |
}, | |
}; | |
return router; | |
}); | |
// Deep Merge Utility for AppState.load function | |
function deepMerge(target, source) { | |
for (const key in source) { | |
if (source.hasOwnProperty(key)) { | |
if (source[key] instanceof Object && !Array.isArray(source[key])) { | |
if (!target[key]) Object.assign(target, { [key]: {} }); | |
deepMerge(target[key], source[key]); | |
} else { | |
Object.assign(target, { [key]: source[key] }); | |
} | |
} | |
} | |
return target; | |
} | |
// State Management | |
const AppState = { | |
data: { | |
userProfile: { | |
name: 'John Doe', | |
email: '[email protected]', | |
darkMode: false, | |
notifications: true, | |
twoFactor: true | |
}, | |
settings: { | |
language: 'en', | |
timezone: 'UTC', | |
autoSave: true, | |
debugMode: false | |
}, | |
notifications: { | |
email: true, | |
sms: false, | |
push: true, | |
mentions: true | |
}, | |
privacy: { | |
locationServices: true, | |
personalizedAds: false | |
} | |
}, | |
get(key) { | |
const keys = key.split('.'); | |
let value = this.data; | |
for (const k of keys) { | |
if (value && typeof value === 'object' && value.hasOwnProperty(k)) { | |
value = value[k]; | |
} else { | |
return undefined; | |
} | |
} | |
return value; | |
}, | |
set(key, value) { | |
const keys = key.split('.'); | |
const lastKey = keys.pop(); | |
let obj = this.data; | |
for (const k of keys) { | |
if (!obj[k] || typeof obj[k] !== 'object') { | |
obj[k] = {}; | |
} | |
obj = obj[k]; | |
} | |
obj[lastKey] = value; | |
this.showStateUpdate(`Updated ${key}: ${JSON.stringify(value)}`); | |
}, | |
toggle(key) { | |
const current = this.get(key); | |
this.set(key, !current); | |
return !current; | |
}, | |
save() { | |
try { | |
const json = JSON.stringify(this.data, null, 2); | |
const blob = new Blob([json], { type: 'application/json' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'app_state.json'; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); | |
this.showStateUpdate('State saved to app_state.json'); | |
} catch (e) { | |
console.error('Failed to save state:', e); | |
this.showStateUpdate('Failed to save state', true); | |
} | |
}, | |
load() { | |
const input = document.createElement('input'); | |
input.type = 'file'; | |
input.accept = 'application/json'; | |
input.onchange = async (e) => { | |
const file = e.target.files[0]; | |
if (!file) return; | |
try { | |
const reader = new FileReader(); | |
reader.onload = (event) => { | |
try { | |
const loadedData = JSON.parse(event.target.result); | |
this.data = deepMerge(this.data, loadedData); | |
this.showStateUpdate('State loaded successfully'); | |
Router.apply(Router.getFragment()); | |
} catch (parseError) { | |
console.error('Error parsing JSON:', parseError); | |
this.showStateUpdate('Error loading state: Invalid JSON', true); | |
} | |
}; | |
reader.readAsText(file); | |
} catch (readError) { | |
console.error('Error reading file:', readError); | |
this.showStateUpdate('Error loading state: File read error', true); | |
} | |
}; | |
input.click(); | |
}, | |
clear() { | |
this.data = { | |
userProfile: { name: 'John Doe', email: '[email protected]', darkMode: false, notifications: true, twoFactor: true }, | |
settings: { language: 'en', timezone: 'UTC', autoSave: true, debugMode: false }, | |
notifications: { email: true, sms: false, push: true, mentions: true }, | |
privacy: { locationServices: true, personalizedAds: false } | |
}; | |
this.showStateUpdate('State cleared to defaults'); | |
Router.apply(Router.getFragment()); | |
}, | |
showStateUpdate(message, isError = false) { | |
const indicator = document.getElementById('stateIndicator'); | |
indicator.textContent = message; | |
indicator.classList.remove('alert-danger', 'alert-info'); | |
indicator.classList.add(isError ? 'alert-danger' : 'alert-info'); | |
indicator.style.display = 'block'; | |
indicator.classList.add('show'); | |
setTimeout(() => { | |
indicator.classList.remove('show'); | |
setTimeout(() => { indicator.style.display = 'none'; }, 300); | |
}, 2000); | |
} | |
}; | |
// Menu Configuration with hierarchical structure (Font Awesome Icons) | |
const MenuConfig = { | |
modal: { | |
profile: { | |
title: 'User Profile', | |
type: 'menu', | |
// Example of a dynamic action for the header badge | |
action: () => { | |
const userName = AppState.get('userProfile.name') || 'User'; | |
return `<span class="badge bg-primary"><i class="fa-solid fa-user-circle me-1"></i>${userName}</span>`; | |
}, | |
items: [ | |
{ label: () => AppState.get('userProfile.name') || 'John Doe', icon: 'fa-solid fa-user-circle', route: 'profile/edit', description: 'Edit your profile information' }, | |
{ label: 'Account Settings', icon: 'fa-solid fa-gear', route: 'profile/settings', description: 'Manage account preferences' }, | |
{ label: 'Security', icon: 'fa-solid fa-shield-halved', route: 'profile/security', description: 'Security and privacy settings' }, | |
{ label: 'Billing', icon: 'fa-solid fa-credit-card', action: () => alert('Billing information is handled externally.') } | |
], | |
footer: '<button class="btn btn-sm btn-outline-danger" onclick="alert(\'Signed out!\')"><i class="fa-solid fa-right-from-bracket me-1"></i>Sign Out</button>' | |
}, | |
'profile/edit': { | |
title: 'Edit Profile', | |
type: 'form', | |
// Header action for the edit form | |
action: '<i class="fa-solid fa-pencil text-secondary"></i>', | |
content: () => ` | |
<div class="profile-edit-form"> | |
<div class="mb-3"> | |
<label for="profileName" class="form-label">Name</label> | |
<input type="text" class="form-control" id="profileName" value="${AppState.get('userProfile.name')}"> | |
</div> | |
<div class="mb-3"> | |
<label for="profileEmail" class="form-label">Email address</label> | |
<input type="email" class="form-control" id="profileEmail" value="${AppState.get('userProfile.email')}"> | |
</div> | |
<div class="btn-group"> | |
<button type="button" class="btn btn-outline-secondary" onclick="Router.back()">Cancel</button> | |
<button type="button" class="btn btn-primary" id="saveProfileBtn">Save Changes</button> | |
</div> | |
</div> | |
`, | |
onRender: () => { | |
const saveBtn = document.getElementById('saveProfileBtn'); | |
if (saveBtn) { | |
saveBtn.onclick = () => { | |
const newName = document.getElementById('profileName').value; | |
const newEmail = document.getElementById('profileEmail').value; | |
AppState.set('userProfile.name', newName); | |
AppState.set('userProfile.email', newEmail); | |
Router.back(); | |
}; | |
} | |
} | |
}, | |
'profile/settings': { | |
title: 'Account Settings', | |
type: 'menu', | |
action: '<span class="badge bg-info"><i class="fa-solid fa-info-circle me-1"></i>Info</span>', | |
items: [ | |
{ type: 'group', title: 'General Options', items: [ | |
{ label: 'Dark Mode', icon: 'fa-solid fa-moon', toggle: true, key: 'userProfile.darkMode' }, | |
{ label: 'Auto Save', icon: 'fa-solid fa-cloud-arrow-up', toggle: true, key: 'settings.autoSave' } | |
]}, | |
{ type: 'group', title: 'Communication', items: [ | |
{ label: 'Notifications', icon: 'fa-solid fa-bell', route: 'profile/settings/notifications', description: 'Manage notification preferences' }, | |
{ label: 'Contact Preferences', icon: 'fa-solid fa-address-book', action: () => alert('Contact preferences editor would open here.') } | |
]} | |
] | |
}, | |
'profile/settings/notifications': { | |
title: 'Notifications', | |
type: 'menu', | |
action: '<i class="fa-solid fa-envelope-open text-muted"></i>', // Simple icon action | |
items: [ | |
{ label: 'Email Notifications', icon: 'fa-solid fa-envelope', toggle: true, key: 'notifications.email' }, | |
{ label: 'SMS Alerts', icon: 'fa-solid fa-mobile-alt', toggle: true, key: 'notifications.sms' }, | |
{ label: 'Push Notifications', icon: 'fa-solid fa-bell-slash', toggle: true, key: 'notifications.push' }, | |
{ label: 'Mentions & Replies', icon: 'fa-solid fa-at', toggle: true, key: 'notifications.mentions' } | |
] | |
}, | |
'profile/security': { | |
title: 'Security Settings', | |
type: 'menu', | |
action: '<span class="badge bg-success"><i class="fa-solid fa-check-circle me-1"></i>Secure</span>', | |
items: [ | |
{ type: 'group', title: 'Authentication', items: [ | |
{ label: 'Two-Factor Authentication', icon: 'fa-solid fa-shield-alt', toggle: true, key: 'userProfile.twoFactor' }, | |
{ label: 'Change Password', icon: 'fa-solid fa-lock', action: () => alert('Password change dialog would open here.') } | |
]}, | |
{ type: 'group', title: 'Activity', items: [ | |
{ label: 'Login History', icon: 'fa-solid fa-history', action: () => alert('Login history would be displayed here.') }, | |
{ label: 'Active Sessions', icon: 'fa-solid fa-desktop', action: () => alert('Active sessions management.') } | |
]} | |
] | |
}, | |
about: { | |
title: 'About Us', | |
type: 'info', | |
action: '<i class="fa-solid fa-info-circle text-primary"></i>', // Action for About Us | |
content: ` | |
<div class="text-center mb-4"> | |
<i class="fa-solid fa-circle-info text-primary" style="font-size: 3rem;"></i> | |
<h3 class="mt-2">Our Company</h3> | |
</div> | |
<p>Welcome to our innovative platform that brings cutting-edge technology to your fingertips.</p> | |
<h5>Our Mission</h5> | |
<p>We strive to create exceptional user experiences through intuitive design and robust functionality. Our team is dedicated to pushing the boundaries of what's possible in web applications.</p> | |
<h5>Features</h5> | |
<ul> | |
<li><i class="fa-solid fa-route me-2 text-info"></i>Advanced routing capabilities</li> | |
<li><i class="fa-solid fa-database me-2 text-info"></i>State management</li> | |
<li><i class="fa-solid fa-mobile-alt me-2 text-info"></i>Responsive design</li> | |
<li><i class="fa-brands fa-bootstrap me-2 text-info"></i>Bootstrap integration</li> | |
</ul> | |
<h5>Contact Information</h5> | |
<div class="row"> | |
<div class="col-md-6 mb-3 mb-md-0"> | |
<strong><i class="fa-solid fa-envelope me-2"></i>Email:</strong><br> | |
<a href="mailto:[email protected]">[email protected]</a> | |
</div> | |
<div class="col-md-6"> | |
<strong><i class="fa-solid fa-phone me-2"></i>Phone:</strong><br> | |
+1 (555) 123-4567 | |
</div> | |
</div> | |
<div class="mt-4 p-3 bg-light rounded text-center"> | |
<small class="text-muted"> Version 2.1.0 • Last updated: July 2025<br> © 2025 Our Company. All rights reserved. </small> | |
</div> | |
` | |
} | |
}, | |
offcanvas: { | |
settings: { | |
title: 'Settings', | |
type: 'menu', | |
action: '<i class="fa-solid fa-gear text-muted me-1"></i><span class="badge bg-warning text-dark">Beta</span>', // Example with badge and icon | |
items: [ | |
{ type: 'group', title: 'General', items: [ | |
{ label: 'General Settings', icon: 'fa-solid fa-sliders', route: 'settings/general', description: 'Basic application settings' }, | |
{ label: 'Appearance', icon: 'fa-solid fa-palette', route: 'settings/appearance', description: 'Customize the look and feel' } | |
]}, | |
{ type: 'group', title: 'Privacy & Data', items: [ | |
{ label: 'Privacy Controls', icon: 'fa-solid fa-user-shield', route: 'settings/privacy', description: 'Privacy controls and data management' }, | |
{ label: 'Data Export/Import', icon: 'fa-solid fa-database', route: 'settings/data-management', description: 'Manage your application data' } | |
]}, | |
{ type: 'group', title: 'Advanced', items: [ | |
{ label: 'Advanced Settings', icon: 'fa-solid fa-gear-code', route: 'settings/advanced', description: 'Advanced configuration options' } | |
]} | |
] | |
}, | |
'settings/general': { | |
title: 'General Settings', | |
type: 'menu', | |
action: '<i class="fa-solid fa-cog text-primary"></i>', // Simple icon for general settings | |
items: [ | |
{ label: () => `Language: ${AppState.get('settings.language').toUpperCase()}`, icon: 'fa-solid fa-language', action: () => { | |
const lang = prompt('Enter language code (en/es/fr):', AppState.get('settings.language')); | |
if (lang) { AppState.set('settings.language', lang); Router.apply(Router.getFragment()); } | |
}}, | |
{ label: () => `Timezone: ${AppState.get('settings.timezone')}`, icon: 'fa-solid fa-earth-americas', action: () => { | |
const tz = prompt('Enter timezone:', AppState.get('settings.timezone')); | |
if (tz) { AppState.set('settings.timezone', tz); Router.apply(Router.getFragment()); } | |
}}, | |
{ label: 'Auto Save', icon: 'fa-solid fa-cloud-arrow-up', toggle: true, key: 'settings.autoSave' } | |
] | |
}, | |
'settings/appearance': { | |
title: 'Appearance', | |
type: 'menu', | |
action: '<i class="fa-solid fa-brush text-info"></i>', | |
items: [ | |
{ label: 'Dark Theme', icon: 'fa-solid fa-moon', toggle: true, key: 'userProfile.darkMode' }, | |
{ label: 'Font Size', icon: 'fa-solid fa-font', action: () => alert('Font size selector would open here.') }, | |
{ label: 'Color Scheme', icon: 'fa-solid fa-fill-drip', action: () => alert('Color scheme picker would open here.') } | |
] | |
}, | |
'settings/privacy': { | |
title: 'Privacy Controls', | |
type: 'menu', | |
action: '<i class="fa-solid fa-lock text-success"></i>', // Green lock icon | |
items: [ | |
{ label: 'Cookie Preferences', icon: 'fa-solid fa-cookie-bite', action: () => alert('Cookie consent management.') }, | |
{ label: 'Location Services', icon: 'fa-solid fa-location-dot', toggle: true, key: 'privacy.locationServices' }, | |
{ label: 'Personalized Ads', icon: 'fa-solid fa-ad', toggle: true, key: 'privacy.personalizedAds' } | |
] | |
}, | |
'settings/data-management': { | |
title: 'Data Management', | |
type: 'menu', | |
action: '<i class="fa-solid fa-hdd text-secondary"></i>', | |
items: [ | |
{ label: 'Export Data', icon: 'fa-solid fa-download', action: () => AppState.save() }, | |
{ label: 'Import Data', icon: 'fa-solid fa-upload', action: () => AppState.load() }, | |
{ label: 'Reset All Data', icon: 'fa-solid fa-eraser', action: () => { | |
if (confirm('Are you sure you want to reset all application data to defaults?')) { | |
AppState.clear(); | |
Router.back(); | |
} | |
}}, | |
] | |
}, | |
'settings/advanced': { | |
title: 'Advanced Settings', | |
type: 'menu', | |
action: '<i class="fa-solid fa-microchip text-danger"></i>', | |
items: [ | |
{ label: 'Debug Mode', icon: 'fa-solid fa-bug', toggle: true, key: 'settings.debugMode' }, | |
{ label: 'Cache Settings', icon: 'fa-solid fa-hdd', action: () => alert('Cache management options.') }, | |
{ label: 'API Keys', icon: 'fa-solid fa-key', action: () => alert('Manage API integration keys.') } | |
] | |
}, | |
account: { | |
title: 'Account Management', | |
type: 'menu', | |
action: '<i class="fa-solid fa-user-gear text-primary"></i>', | |
items: [ | |
{ type: 'group', title: 'Profile', items: [ | |
{ label: 'Profile Overview', icon: 'fa-solid fa-user-check', route: 'account/profile', description: 'View and edit profile details' }, | |
{ label: 'Account Security', icon: 'fa-solid fa-shield-alt', route: 'account/security-offcanvas', description: 'Security and login settings' } | |
]}, | |
{ type: 'group', title: 'Financial', items: [ | |
{ label: 'Subscription', icon: 'fa-solid fa-credit-card', route: 'account/billing', description: 'Manage your subscription' }, | |
{ label: 'Payment Methods', icon: 'fa-solid fa-wallet', action: () => alert('Manage stored payment methods.') } | |
]}, | |
{ type: 'group', title: 'Data', items: [ | |
{ label: 'Data & Privacy', icon: 'fa-solid fa-shield-alt', route: 'account/privacy', description: 'Privacy and data settings' } | |
]} | |
], | |
footer: '<button class="btn btn-sm btn-warning" onclick="alert(\'Account deactivation process would begin.\')"><i class="fa-solid fa-triangle-exclamation me-1"></i>Deactivate Account</button>' | |
}, | |
'account/profile': { | |
title: 'Profile Overview', | |
type: 'menu', | |
action: '<i class="fa-solid fa-id-card text-success"></i>', | |
items: [ | |
{ label: () => `Display Name: ${AppState.get('userProfile.name')}`, icon: 'fa-solid fa-user', action: () => { | |
const name = prompt('Update display name:', AppState.get('userProfile.name')); | |
if (name) { AppState.set('userProfile.name', name); Router.apply(Router.getFragment()); } | |
}}, | |
{ label: () => `Contact Email: ${AppState.get('userProfile.email')}`, icon: 'fa-solid fa-at', action: () => { | |
const email = prompt('Update email:', AppState.get('userProfile.email')); | |
if (email) { AppState.set('userProfile.email', email); Router.apply(Router.getFragment()); } | |
}}, | |
{ label: 'Profile Picture', icon: 'fa-solid fa-camera', action: () => alert('Profile picture upload would open here.') } | |
] | |
}, | |
'account/security-offcanvas': { | |
title: 'Account Security', | |
type: 'menu', | |
action: '<i class="fa-solid fa-fingerprint text-danger"></i>', | |
items: [ | |
{ label: 'Change Password', icon: 'fa-solid fa-lock', action: () => alert('Change password dialog.') }, | |
{ label: 'Two-Factor Authentication', icon: 'fa-solid fa-shield-alt', toggle: true, key: 'userProfile.twoFactor' }, | |
{ label: 'View Login History', icon: 'fa-solid fa-history', action: () => alert('View recent logins.') } | |
] | |
}, | |
'account/billing': { | |
title: 'Subscription & Billing', | |
type: 'menu', | |
action: '<span class="badge bg-success"><i class="fa-solid fa-check me-1"></i>Active</span>', | |
items: [ | |
{ label: 'Current Plan: Pro', icon: 'fa-solid fa-gem', action: () => alert('Plan details and upgrade options.') }, | |
{ label: 'Payment Method', icon: 'fa-solid fa-credit-card', action: () => alert('Payment method management.') }, | |
{ label: 'Billing History', icon: 'fa-solid fa-receipt', action: () => alert('Download billing history.') } | |
] | |
}, | |
'account/privacy': { | |
title: 'Data & Privacy', | |
type: 'menu', | |
action: '<i class="fa-solid fa-user-secret text-secondary"></i>', | |
items: [ | |
{ label: 'Data Export', icon: 'fa-solid fa-file-export', action: () => AppState.save() }, | |
{ label: 'Privacy Settings', icon: 'fa-solid fa-eye-slash', action: () => alert('Privacy controls would open here.') }, | |
{ label: 'Delete All Data', icon: 'fa-solid fa-trash', action: () => { | |
if (confirm('Are you sure you want to delete all data? This action cannot be undone.')) { | |
AppState.clear(); | |
if (currentModalInstance) currentModalInstance.hide(); | |
if (currentOffcanvasInstance) currentOffcanvasInstance.hide(); | |
Router.navigate(''); | |
} | |
}} | |
] | |
} | |
} | |
}; | |
// Menu Renderer Class (No major changes needed here for component positioning, as it's handled by Router) | |
class MenuRenderer { | |
constructor(containerId, headerTitleSelector, headerBackSelector, headerActionSelector, type, bsComponent) { | |
this.container = document.getElementById(containerId); | |
this.titleElement = document.querySelector(headerTitleSelector); | |
this.backButton = document.getElementById(headerBackSelector); | |
this.headerAction = document.getElementById(headerActionSelector); | |
this.type = type; | |
this.bsComponent = bsComponent; | |
this.previousPath = ''; | |
if (this.backButton) { | |
this.backButton.onclick = () => { Router.back(); }; | |
} | |
if (bsComponent && bsComponent._element) { | |
bsComponent._element.addEventListener('hidden.bs.modal', () => this.reset()); | |
bsComponent._element.addEventListener('hidden.bs.offcanvas', () => this.reset()); | |
} | |
} | |
reset() { | |
if (this.container) this.container.innerHTML = ''; | |
this.previousPath = ''; | |
if (this.titleElement) this.titleElement.textContent = 'Menu'; | |
if (this.backButton) this.backButton.style.display = 'none'; | |
if (this.headerAction) this.headerAction.innerHTML = ''; | |
} | |
render(path) { | |
const config = MenuConfig[this.type][path]; | |
if (!config) { | |
console.warn(`Menu config not found for path: ${this.type}/${path}. Attempting to navigate back.`); | |
if (path.includes('/')) { | |
const pathParts = path.split('/'); | |
pathParts.pop(); | |
const parentPath = pathParts.join('/'); | |
if (MenuConfig[this.type][parentPath]) { | |
Router.navigate(`${this.type}/${parentPath}`, Router.__currentNavigationOptions); // Pass options on fallback | |
return; | |
} | |
} | |
this.close(); | |
Router.navigate(''); | |
return; | |
} | |
let isForward; | |
if (!this.previousPath) { | |
isForward = true; | |
} else if (path.startsWith(this.previousPath + '/')) { | |
isForward = true; | |
} else if (this.previousPath.startsWith(path + '/') && this.previousPath.split('/').length === path.split('/').length + 1) { | |
isForward = false; | |
} else { | |
isForward = true; | |
} | |
this.updateHeader(config, path); | |
if (config.type === 'info') { | |
this.renderInfoContent(config); | |
} else if (config.type === 'form') { | |
this.renderFormContent(config); | |
} else { | |
this.renderMenu(path, config, isForward); | |
} | |
this.previousPath = path; | |
} | |
updateHeader(config, currentFullPath) { | |
if (this.titleElement) { | |
this.titleElement.textContent = config.title; | |
} | |
if (this.backButton) { | |
const pathParts = currentFullPath.split('/'); | |
if (pathParts.length > 1) { | |
this.backButton.style.display = 'flex'; | |
this.backButton.onclick = () => { Router.back(); }; | |
} else { | |
this.backButton.style.display = 'none'; | |
this.backButton.onclick = null; | |
} | |
} | |
if (this.headerAction) { | |
if (config.action) { | |
this.headerAction.innerHTML = typeof config.action === 'function' ? config.action() : config.action; | |
// Add click handler to action if it's dynamic, but only if it's a function. | |
// For simple HTML, we don't need a wrapper click. | |
if (typeof config.action === 'function') { | |
this.headerAction.querySelectorAll('*').forEach(el => { | |
el.style.cursor = 'pointer'; // Indicate clickability | |
el.onclick = (e) => { | |
e.stopPropagation(); | |
config.action(); // Execute the action function if it's a function | |
}; | |
}); | |
} | |
} else { | |
this.headerAction.innerHTML = ''; | |
} | |
} | |
} | |
renderInfoContent(config) { | |
if (!this.container) return; | |
const infoLevel = document.createElement('div'); | |
infoLevel.className = 'menu-level active'; | |
infoLevel.innerHTML = `<div class="info-content">${config.content}</div>`; | |
this.container.innerHTML = ''; | |
this.container.appendChild(infoLevel); | |
} | |
renderFormContent(config) { | |
if (!this.container) return; | |
const formLevel = document.createElement('div'); | |
formLevel.className = 'menu-level active'; | |
formLevel.innerHTML = typeof config.content === 'function' ? config.content() : config.content; | |
this.container.innerHTML = ''; | |
this.container.appendChild(formLevel); | |
if (typeof config.onRender === 'function') { | |
config.onRender(); | |
} | |
} | |
renderMenu(path, config, isForward) { | |
if (!this.container) return; | |
const newLevel = document.createElement('div'); | |
newLevel.className = 'menu-level'; | |
const itemsContainer = document.createElement('div'); | |
itemsContainer.style.cssText = `height: ${config.footer ? 'calc(100% - 60px)' : '100%'}; overflow-y: auto;`; | |
config.items.forEach(item => { | |
if (item.type === 'group') { | |
const groupTitleEl = document.createElement('div'); | |
groupTitleEl.className = 'menu-group-title'; | |
groupTitleEl.textContent = item.title; | |
itemsContainer.appendChild(groupTitleEl); | |
item.items.forEach(groupItem => { | |
const itemEl = this.createMenuItem(path, groupItem); | |
itemsContainer.appendChild(itemEl); | |
}); | |
} else { | |
const itemEl = this.createMenuItem(path, item); | |
itemsContainer.appendChild(itemEl); | |
} | |
}); | |
newLevel.appendChild(itemsContainer); | |
if (config.footer) { | |
const footerEl = document.createElement('div'); | |
footerEl.className = 'menu-footer'; | |
footerEl.innerHTML = config.footer; | |
newLevel.appendChild(footerEl); | |
} | |
this.container.appendChild(newLevel); | |
this.animateLevel(newLevel, isForward); | |
} | |
createMenuItem(currentMenuPath, item) { | |
const itemEl = document.createElement('div'); | |
itemEl.className = 'menu-item'; | |
if (item.disabled) itemEl.classList.add('disabled'); | |
itemEl.setAttribute('role', 'menuitem'); | |
let labelText = typeof item.label === 'function' ? item.label() : item.label; | |
let iconHtml = item.icon ? `<i class="${item.icon} fa-fw" aria-hidden="true"></i>` : ''; | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'menu-item-content'; | |
contentDiv.innerHTML = `${iconHtml}<span>${labelText}</span>`; | |
if (item.description) { | |
const descriptionSpan = document.createElement('small'); | |
descriptionSpan.className = 'd-block menu-item-description'; | |
contentDiv.querySelector('span').appendChild(descriptionSpan); | |
descriptionSpan.textContent = item.description; | |
} | |
itemEl.appendChild(contentDiv); | |
if (item.route) { | |
itemEl.innerHTML += '<i class="fa-solid fa-chevron-right text-muted" aria-hidden="true"></i>'; | |
itemEl.onclick = () => { | |
if (!item.disabled) { | |
Router.navigate(`${this.type}/${item.route}`, Router.__currentNavigationOptions); // Pass options to submenu navigation | |
} | |
}; | |
} else if (item.toggle) { | |
const toggleKey = item.key; | |
if (!toggleKey) { console.error('Toggle item missing key:', item); return itemEl; } | |
const toggleId = toggleKey.replace(/\./g, '-'); | |
const toggleSwitchDiv = document.createElement('div'); | |
toggleSwitchDiv.className = 'form-check form-switch'; | |
toggleSwitchDiv.innerHTML = ` | |
<input class="form-check-input" type="checkbox" role="switch" id="${toggleId}" ${AppState.get(toggleKey) ? 'checked' : ''} aria-label="Toggle ${labelText}"> | |
`; | |
const toggleInput = toggleSwitchDiv.querySelector('input'); | |
toggleInput.onchange = (e) => { | |
e.stopPropagation(); | |
AppState.toggle(toggleKey); | |
if (typeof item.label === 'function') { contentDiv.querySelector('span').firstChild.nodeValue = item.label(); } | |
}; | |
itemEl.appendChild(toggleSwitchDiv); | |
itemEl.onclick = (e) => { | |
if (!item.disabled && e.target !== toggleInput) { | |
toggleInput.checked = !toggleInput.checked; | |
AppState.toggle(toggleKey); | |
if (typeof item.label === 'function') { contentDiv.querySelector('span').firstChild.nodeValue = item.label(); } | |
} | |
}; | |
} else if (item.action) { | |
itemEl.innerHTML += '<i class="fa-solid fa-arrow-up-right-from-square text-muted" aria-hidden="true"></i>'; | |
itemEl.onclick = () => { | |
if (!item.disabled) { | |
if (typeof item.action === 'function') { | |
item.action(); | |
} else { | |
alert(`Action for: ${item.label}`); | |
} | |
} | |
}; | |
} | |
if (item.action || item.toggle) { | |
const chevron = itemEl.querySelector('.fa-chevron-right'); | |
if (chevron) chevron.remove(); | |
} | |
return itemEl; | |
} | |
animateLevel(newLevel, isForward) { | |
if (!this.container) return; | |
const currentActiveLevel = this.container.querySelector('.menu-level.active'); | |
if (currentActiveLevel && currentActiveLevel !== newLevel) { | |
newLevel.classList.add(isForward ? 'slide-right' : 'slide-left'); | |
newLevel.style.zIndex = 2; | |
newLevel.offsetWidth; // Trigger reflow | |
currentActiveLevel.classList.remove('active'); | |
currentActiveLevel.classList.add(isForward ? 'slide-left' : 'slide-right'); | |
currentActiveLevel.style.zIndex = 1; | |
newLevel.classList.add('active'); | |
newLevel.classList.remove(isForward ? 'slide-right' : 'slide-left'); | |
const onTransitionEnd = () => { | |
currentActiveLevel.removeEventListener('transitionend', onTransitionEnd); | |
currentActiveLevel.remove(); | |
}; | |
currentActiveLevel.addEventListener('transitionend', onTransitionEnd, { once: true }); | |
} else { | |
newLevel.classList.add('active'); | |
} | |
} | |
close() { | |
if (this.bsComponent) { | |
this.bsComponent.hide(); | |
} | |
} | |
} | |
// Initialize Bootstrap Modals/Offcanvas | |
const menuModalEl = document.getElementById('menuModal'); | |
const menuOffcanvasEl = document.getElementById('menuOffcanvas'); | |
let currentModalInstance = null; | |
let currentOffcanvasInstance = null; | |
if (menuModalEl) { currentModalInstance = new bootstrap.Modal(menuModalEl); } | |
if (menuOffcanvasEl) { currentOffcanvasInstance = new bootstrap.Offcanvas(menuOffcanvasEl); } | |
// Initialize Menu Renderers. | |
const modalMenuRenderer = new MenuRenderer( | |
'modalMenuContainer', | |
'#menuModalLabel', | |
'modalBackButton', | |
'#modalHeaderAction', | |
'modal', currentModalInstance | |
); | |
const offcanvasMenuRenderer = new MenuRenderer( | |
'offcanvasMenuContainer', | |
'#offcanvasMenuLabel', | |
'offcanvasBackButton', | |
'#offcanvasHeaderAction', | |
'offcanvas', currentOffcanvasInstance | |
); | |
// Define default positions | |
const defaultModalPosition = 'center'; // 'top', 'bottom', 'center', 'fullscreen' | |
const defaultOffcanvasPosition = 'end'; // 'top', 'bottom', 'start', 'end', 'fullscreen' | |
// Router Setup (Modified to handle positioning) | |
Router.add('modal/:path*', (matches, params) => { | |
const fullPath = (Array.isArray(params.path) ? params.path.join('/') : params.path) || ''; | |
const config = MenuConfig.modal[fullPath]; | |
if (!config) { | |
if (fullPath.includes('/')) { | |
const pathParts = fullPath.split('/'); | |
pathParts.pop(); | |
const parentPath = pathParts.join('/'); | |
if (MenuConfig.modal[parentPath]) { | |
Router.navigate(`modal/${parentPath}`, { position: Router.__currentNavigationOptions?.position || defaultModalPosition }); | |
return; | |
} | |
} | |
modalMenuRenderer.close(); | |
Router.navigate(''); | |
return; | |
} | |
// Get position from options or use default | |
const position = Router.__currentNavigationOptions?.position || defaultModalPosition; | |
Router.__currentNavigationOptions = {}; // Clear options after use | |
// Apply Bootstrap modal position classes | |
const modalDialog = menuModalEl.querySelector('.modal-dialog'); | |
// Remove all possible positioning classes before adding the new one | |
modalDialog.classList.remove('modal-dialog-centered', 'modal-fullscreen', 'modal-top', 'modal-bottom'); | |
// Apply the chosen position class | |
if (position === 'fullscreen') { | |
modalDialog.classList.add('modal-fullscreen'); | |
} else if (position === 'top') { | |
modalDialog.classList.add('modal-top'); | |
} else if (position === 'bottom') { | |
modalDialog.classList.add('modal-bottom'); | |
} else { // 'center' or any unrecognized position will default to 'modal-dialog-centered' | |
modalDialog.classList.add('modal-dialog-centered'); | |
} | |
modalMenuRenderer.render(fullPath); | |
if (currentModalInstance) currentModalInstance.show(); | |
document.getElementById('currentRoute').textContent = `#modal/${fullPath}`; | |
}); | |
Router.add('offcanvas/:path*', (matches, params) => { | |
const fullPath = (Array.isArray(params.path) ? params.path.join('/') : params.path) || ''; | |
const config = MenuConfig.offcanvas[fullPath]; | |
if (!config) { | |
if (fullPath.includes('/')) { | |
const pathParts = fullPath.split('/'); | |
pathParts.pop(); | |
const parentPath = pathParts.join('/'); | |
if (MenuConfig.offcanvas[parentPath]) { | |
Router.navigate(`offcanvas/${parentPath}`, { position: Router.__currentNavigationOptions?.position || defaultOffcanvasPosition }); | |
return; | |
} | |
} | |
offcanvasMenuRenderer.close(); | |
Router.navigate(''); | |
return; | |
} | |
// Get position from options or use default | |
const position = Router.__currentNavigationOptions?.position || defaultOffcanvasPosition; | |
Router.__currentNavigationOptions = {}; // Clear options after use | |
// Apply Bootstrap offcanvas position classes | |
// Remove all possible positioning classes before adding the new one | |
menuOffcanvasEl.classList.remove('offcanvas-top', 'offcanvas-bottom', 'offcanvas-start', 'offcanvas-end', 'offcanvas-fullscreen'); | |
menuOffcanvasEl.classList.add(`offcanvas-${position}`); // Bootstrap uses these classes for animation | |
offcanvasMenuRenderer.render(fullPath); | |
if (currentOffcanvasInstance) currentOffcanvasInstance.show(); | |
document.getElementById('currentRoute').textContent = `#offcanvas/${fullPath}`; | |
}); | |
Router.add('', () => { | |
if (currentModalInstance) currentModalInstance.hide(); | |
if (currentOffcanvasInstance) currentOffcanvasInstance.hide(); | |
document.getElementById('currentRoute').textContent = '#'; | |
}); | |
// Start the router | |
document.addEventListener('DOMContentLoaded', () => { | |
Router.start(); | |
document.getElementById('currentRoute').textContent = Router.getFragment() || '#'; | |
Router.apply(); | |
}); | |
// Global functions for state management (for button clicks) | |
function saveAppState() { AppState.save(); } | |
function loadAppState() { AppState.load(); } | |
function clearAppState() { AppState.clear(); } | |
// Ensure closing modal/offcanvas also updates hash (makes URL consistent) | |
if (menuModalEl) { | |
menuModalEl.addEventListener('hidden.bs.modal', () => { | |
if (Router.getFragment().startsWith('modal/')) { | |
Router.navigate(''); | |
} | |
}); | |
} | |
if (menuOffcanvasEl) { | |
menuOffcanvasEl.addEventListener('hidden.bs.offcanvas', () => { | |
if (Router.getFragment().startsWith('offcanvas/')) { | |
Router.navigate(''); | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment