Add diy replacement for turbo
This commit is contained in:
68
app/static/js/diy-turbo.js
Normal file
68
app/static/js/diy-turbo.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
document.addEventListener('click', async (e) => {
|
||||||
|
const link = e.target.closest('a');
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
const url = link.getAttribute('href');
|
||||||
|
if (!url || url.startsWith('#') || url.startsWith('javascript:')) return;
|
||||||
|
|
||||||
|
// Only intercept same-origin links
|
||||||
|
if (link.origin !== window.location.origin) return;
|
||||||
|
|
||||||
|
// Ignore links that open in a new tab, download, or modifier keys
|
||||||
|
if (link.target === '_blank' || link.hasAttribute('download')) return;
|
||||||
|
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey) return;
|
||||||
|
|
||||||
|
// Optional: add a "data-turbo='false'" attribute check to disable it on specific links
|
||||||
|
if (link.getAttribute('data-turbo') === 'false') return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
document.body.style.cursor = 'wait';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('Fetch failed');
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
|
document.title = doc.title;
|
||||||
|
document.body.innerHTML = doc.body.innerHTML;
|
||||||
|
|
||||||
|
// Carry over classes on the body if they changed
|
||||||
|
document.body.className = doc.body.className;
|
||||||
|
|
||||||
|
document.body.style.cursor = 'default';
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
// Dispatch a custom event so other scripts can re-initialize if necessary
|
||||||
|
document.dispatchEvent(new Event('diy-turbo:load'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DIY Turbo navigation error:', error);
|
||||||
|
window.location.href = url; // fallback
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', async () => {
|
||||||
|
document.body.style.cursor = 'wait';
|
||||||
|
try {
|
||||||
|
const response = await fetch(window.location.href);
|
||||||
|
if (!response.ok) throw new Error('Fetch failed');
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
|
document.title = doc.title;
|
||||||
|
document.body.innerHTML = doc.body.innerHTML;
|
||||||
|
document.body.className = doc.body.className;
|
||||||
|
document.body.style.cursor = 'default';
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event('diy-turbo:load'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DIY Turbo popstate error:', error);
|
||||||
|
window.location.reload(); // fallback
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>{% block title %}BP Tracker{% endblock %}</title>
|
<title>{% block title %}BP Tracker{% endblock %}</title>
|
||||||
<link rel="icon" type="image/svg+xml" sizes="any" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
<link rel="icon" type="image/svg+xml" sizes="any" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
||||||
<link href="/static/css/tailwind.css" rel="stylesheet">
|
<link href="/static/css/tailwind.css" rel="stylesheet">
|
||||||
|
<script src="{{ url_for('static', filename='js/diy-turbo.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-50 text-gray-800 font-sans antialiased">
|
<body class="bg-gray-50 text-gray-800 font-sans antialiased">
|
||||||
@@ -128,26 +129,25 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// Use document for event delegation since body is replaced by diy-turbo
|
||||||
// Flash messages
|
document.addEventListener('click', async (e) => {
|
||||||
document.querySelectorAll('.flash-close-btn').forEach(btn => {
|
// Flash messages close button
|
||||||
btn.addEventListener('click', (e) => {
|
const flashCloseBtn = e.target.closest('.flash-close-btn');
|
||||||
const el = e.target.closest('.flash-message');
|
if (flashCloseBtn) {
|
||||||
if (el) {
|
const el = flashCloseBtn.closest('.flash-message');
|
||||||
el.style.opacity = '0';
|
if (el) {
|
||||||
setTimeout(() => el.remove(), 300);
|
el.style.opacity = '0';
|
||||||
}
|
setTimeout(() => el.remove(), 300);
|
||||||
});
|
}
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Micro-HTMX implementation
|
// Micro-HTMX implementation
|
||||||
document.body.addEventListener('click', async (e) => {
|
const htmxTrigger = e.target.closest('[hx-get]');
|
||||||
const trigger = e.target.closest('[hx-get]');
|
if (htmxTrigger) {
|
||||||
if (!trigger) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const url = trigger.getAttribute('hx-get');
|
const url = htmxTrigger.getAttribute('hx-get');
|
||||||
const targetSelector = trigger.getAttribute('hx-target');
|
const targetSelector = htmxTrigger.getAttribute('hx-target');
|
||||||
if (!url || !targetSelector) return;
|
if (!url || !targetSelector) return;
|
||||||
|
|
||||||
const targetEl = document.querySelector(targetSelector);
|
const targetEl = document.querySelector(targetSelector);
|
||||||
@@ -160,19 +160,20 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Micro-HTMX error:', err);
|
console.error('Micro-HTMX error:', err);
|
||||||
}
|
}
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile Menu
|
// Mobile Menu Toggle Button
|
||||||
const menuBtn = document.getElementById('mobile-menu-btn');
|
const menuBtn = e.target.closest('#mobile-menu-btn');
|
||||||
const menu = document.getElementById('mobile-menu');
|
if (menuBtn) {
|
||||||
const nav = document.getElementById('mobile-nav');
|
const menu = document.getElementById('mobile-menu');
|
||||||
const iconMenu = document.getElementById('icon-menu');
|
const nav = document.getElementById('mobile-nav');
|
||||||
const iconClose = document.getElementById('icon-close');
|
const iconMenu = document.getElementById('icon-menu');
|
||||||
|
const iconClose = document.getElementById('icon-close');
|
||||||
|
|
||||||
if (menuBtn && menu) {
|
if (menu) {
|
||||||
const toggleMenu = (forceClose = false) => {
|
|
||||||
const isHidden = menu.classList.contains('hidden');
|
const isHidden = menu.classList.contains('hidden');
|
||||||
if (!isHidden || forceClose) {
|
if (!isHidden) {
|
||||||
menu.classList.add('hidden');
|
menu.classList.add('hidden');
|
||||||
nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||||
iconMenu.classList.remove('hidden');
|
iconMenu.classList.remove('hidden');
|
||||||
@@ -185,15 +186,43 @@
|
|||||||
iconClose.classList.remove('hidden');
|
iconClose.classList.remove('hidden');
|
||||||
menuBtn.classList.add('rotate-180', 'transform');
|
menuBtn.classList.add('rotate-180', 'transform');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
menuBtn.addEventListener('click', () => toggleMenu());
|
// Handle clicks outside of nav/menu to close mobile menu
|
||||||
document.addEventListener('click', (e) => {
|
const nav = document.getElementById('mobile-nav');
|
||||||
if (!nav.contains(e.target) && !menu.classList.contains('hidden')) toggleMenu(true);
|
const menu = document.getElementById('mobile-menu');
|
||||||
});
|
if (nav && menu && !nav.contains(e.target) && !menu.classList.contains('hidden')) {
|
||||||
document.addEventListener('keydown', (e) => {
|
menu.classList.add('hidden');
|
||||||
if (e.key === 'Escape' && !menu.classList.contains('hidden')) toggleMenu(true);
|
nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||||
});
|
const iconMenu = document.getElementById('icon-menu');
|
||||||
|
const iconClose = document.getElementById('icon-close');
|
||||||
|
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||||
|
|
||||||
|
if (iconMenu) iconMenu.classList.remove('hidden');
|
||||||
|
if (iconClose) iconClose.classList.add('hidden');
|
||||||
|
if (mobileMenuBtn) mobileMenuBtn.classList.remove('rotate-180', 'transform');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Escape key to close mobile menu
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const menu = document.getElementById('mobile-menu');
|
||||||
|
if (menu && !menu.classList.contains('hidden')) {
|
||||||
|
const nav = document.getElementById('mobile-nav');
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
if (nav) nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||||
|
|
||||||
|
const iconMenu = document.getElementById('icon-menu');
|
||||||
|
const iconClose = document.getElementById('icon-close');
|
||||||
|
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||||
|
|
||||||
|
if (iconMenu) iconMenu.classList.remove('hidden');
|
||||||
|
if (iconClose) iconClose.classList.add('hidden');
|
||||||
|
if (mobileMenuBtn) mobileMenuBtn.classList.remove('rotate-180', 'transform');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,38 +114,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// Use event delegation for dashboard interactions to survive DIY Turbo page replacements
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
// Filter Form Toggle
|
// Filter Form Toggle
|
||||||
const filterBtn = document.getElementById('filter-btn');
|
const filterBtn = e.target.closest('#filter-btn');
|
||||||
const filterForm = document.getElementById('filter-form');
|
if (filterBtn) {
|
||||||
const iconClosed = document.getElementById('filter-icon-closed');
|
const filterForm = document.getElementById('filter-form');
|
||||||
const iconOpen = document.getElementById('filter-icon-open');
|
const iconClosed = document.getElementById('filter-icon-closed');
|
||||||
|
const iconOpen = document.getElementById('filter-icon-open');
|
||||||
|
|
||||||
if (filterBtn && filterForm) {
|
if (filterForm) {
|
||||||
filterBtn.addEventListener('click', () => {
|
|
||||||
const isHidden = filterForm.classList.contains('hidden');
|
const isHidden = filterForm.classList.contains('hidden');
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
filterForm.classList.remove('hidden');
|
filterForm.classList.remove('hidden');
|
||||||
iconClosed.classList.add('hidden');
|
if (iconClosed) iconClosed.classList.add('hidden');
|
||||||
iconOpen.classList.remove('hidden');
|
if (iconOpen) iconOpen.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
filterForm.classList.add('hidden');
|
filterForm.classList.add('hidden');
|
||||||
iconClosed.classList.remove('hidden');
|
if (iconClosed) iconClosed.classList.remove('hidden');
|
||||||
iconOpen.classList.add('hidden');
|
if (iconOpen) iconOpen.classList.add('hidden');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
const tabBtn = e.target.closest('.tab-btn');
|
||||||
tabBtns.forEach(btn => {
|
if (tabBtn) {
|
||||||
btn.addEventListener('click', () => {
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||||
tabBtns.forEach(b => {
|
tabBtns.forEach(b => {
|
||||||
b.classList.remove('border-primary-600', 'text-primary-600');
|
b.classList.remove('border-primary-600', 'text-primary-600');
|
||||||
});
|
|
||||||
btn.classList.add('border-primary-600', 'text-primary-600');
|
|
||||||
});
|
});
|
||||||
});
|
tabBtn.classList.add('border-primary-600', 'text-primary-600');
|
||||||
|
// The click will still bubble up and be caught by the Micro-HTMX logic in _layout.html
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user