mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 19:11:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
* - Mobile menu functionality
|
||||
* - Flash message handling
|
||||
* - Tooltip initialization
|
||||
* - Global HTMX loading state management
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
@@ -171,15 +172,139 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
tooltipEl.className = 'absolute z-50 px-2 py-1 text-sm text-white bg-gray-900 rounded tooltip';
|
||||
tooltipEl.textContent = text;
|
||||
document.body.appendChild(tooltipEl);
|
||||
|
||||
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
tooltipEl.style.top = rect.bottom + 5 + 'px';
|
||||
tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px';
|
||||
});
|
||||
|
||||
|
||||
tooltip.addEventListener('mouseleave', () => {
|
||||
const tooltips = document.querySelectorAll('.tooltip');
|
||||
tooltips.forEach(t => t.remove());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// HTMX Loading State Management
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Global HTMX Loading State Management
|
||||
*
|
||||
* Provides consistent loading state handling across the application:
|
||||
* - Adds 'htmx-loading' class to body during requests
|
||||
* - Manages button disabled states during form submissions
|
||||
* - Handles search input loading states with debouncing
|
||||
* - Provides skeleton screen swap utilities
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Track active HTMX requests
|
||||
let activeRequests = 0;
|
||||
|
||||
/**
|
||||
* Add global loading class to body during HTMX requests
|
||||
*/
|
||||
document.body.addEventListener('htmx:beforeRequest', (evt) => {
|
||||
activeRequests++;
|
||||
document.body.classList.add('htmx-loading');
|
||||
|
||||
// Disable submit buttons within the target element
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'FORM' || target.closest('form')) {
|
||||
const form = target.tagName === 'FORM' ? target : target.closest('form');
|
||||
const submitBtn = form.querySelector('[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('htmx-request');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove global loading class when request completes
|
||||
*/
|
||||
document.body.addEventListener('htmx:afterRequest', (evt) => {
|
||||
activeRequests--;
|
||||
if (activeRequests <= 0) {
|
||||
activeRequests = 0;
|
||||
document.body.classList.remove('htmx-loading');
|
||||
}
|
||||
|
||||
// Re-enable submit buttons
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'FORM' || target.closest('form')) {
|
||||
const form = target.tagName === 'FORM' ? target : target.closest('form');
|
||||
const submitBtn = form.querySelector('[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('htmx-request');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle search inputs with loading states
|
||||
* Automatically adds loading indicator to search inputs during HTMX requests
|
||||
*/
|
||||
document.querySelectorAll('input[type="search"], input[data-search]').forEach(input => {
|
||||
const wrapper = input.closest('.search-wrapper, .relative');
|
||||
if (!wrapper) return;
|
||||
|
||||
// Create loading indicator if it doesn't exist
|
||||
let indicator = wrapper.querySelector('.search-loading');
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('span');
|
||||
indicator.className = 'search-loading htmx-indicator absolute right-3 top-1/2 -translate-y-1/2';
|
||||
indicator.innerHTML = '<i class="fas fa-spinner fa-spin text-muted-foreground"></i>';
|
||||
wrapper.appendChild(indicator);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Swap skeleton with content utility
|
||||
* Use data-skeleton-target to specify which skeleton to hide when content loads
|
||||
*/
|
||||
document.body.addEventListener('htmx:afterSwap', (evt) => {
|
||||
const skeletonTarget = evt.target.dataset.skeletonTarget;
|
||||
if (skeletonTarget) {
|
||||
const skeleton = document.querySelector(skeletonTarget);
|
||||
if (skeleton) {
|
||||
skeleton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility function to show skeleton and trigger HTMX load
|
||||
* @param {string} targetId - ID of the target element
|
||||
* @param {string} skeletonId - ID of the skeleton element
|
||||
*/
|
||||
window.showSkeletonAndLoad = function(targetId, skeletonId) {
|
||||
const target = document.getElementById(targetId);
|
||||
const skeleton = document.getElementById(skeletonId);
|
||||
|
||||
if (skeleton) {
|
||||
skeleton.style.display = 'block';
|
||||
}
|
||||
|
||||
if (target) {
|
||||
htmx.trigger(target, 'load');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to replace content with skeleton during reload
|
||||
* @param {string} targetId - ID of the target element
|
||||
* @param {string} skeletonHtml - HTML string of skeleton to show
|
||||
*/
|
||||
window.reloadWithSkeleton = function(targetId, skeletonHtml) {
|
||||
const target = document.getElementById(targetId);
|
||||
if (target && skeletonHtml) {
|
||||
// Store original content temporarily
|
||||
target.dataset.originalContent = target.innerHTML;
|
||||
target.innerHTML = skeletonHtml;
|
||||
htmx.trigger(target, 'load');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user