fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
388
resources/js/modules/livecomponent/TooltipManager.js
Normal file
388
resources/js/modules/livecomponent/TooltipManager.js
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* Tooltip Manager for LiveComponents
|
||||
*
|
||||
* Provides tooltip functionality for LiveComponent elements with:
|
||||
* - Automatic positioning
|
||||
* - Validation error tooltips
|
||||
* - Accessibility support
|
||||
* - Smooth animations
|
||||
* - Multiple positioning strategies
|
||||
*/
|
||||
|
||||
export class TooltipManager {
|
||||
constructor() {
|
||||
this.tooltips = new Map(); // element → tooltip element
|
||||
this.activeTooltip = null;
|
||||
this.config = {
|
||||
delay: 300, // ms before showing tooltip
|
||||
hideDelay: 100, // ms before hiding tooltip
|
||||
maxWidth: 300, // px
|
||||
offset: 8, // px offset from element
|
||||
animationDuration: 200, // ms
|
||||
zIndex: 10000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tooltip for element
|
||||
*
|
||||
* @param {HTMLElement} element - Element to attach tooltip to
|
||||
* @param {Object} options - Tooltip options
|
||||
*/
|
||||
init(element, options = {}) {
|
||||
// Get tooltip content from data attributes or options
|
||||
const content = options.content ||
|
||||
element.dataset.tooltip ||
|
||||
element.getAttribute('title') ||
|
||||
element.getAttribute('aria-label') ||
|
||||
'';
|
||||
|
||||
if (!content) {
|
||||
return; // No tooltip content
|
||||
}
|
||||
|
||||
// Remove native title attribute to prevent default tooltip
|
||||
if (element.hasAttribute('title')) {
|
||||
element.dataset.originalTitle = element.getAttribute('title');
|
||||
element.removeAttribute('title');
|
||||
}
|
||||
|
||||
// Get positioning
|
||||
const position = options.position ||
|
||||
element.dataset.tooltipPosition ||
|
||||
'top';
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners(element, content, position, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - Element
|
||||
* @param {string} content - Tooltip content
|
||||
* @param {string} position - Position (top, bottom, left, right)
|
||||
* @param {Object} options - Additional options
|
||||
*/
|
||||
setupEventListeners(element, content, position, options) {
|
||||
let showTimeout;
|
||||
let hideTimeout;
|
||||
|
||||
const showTooltip = () => {
|
||||
clearTimeout(hideTimeout);
|
||||
showTimeout = setTimeout(() => {
|
||||
this.show(element, content, position, options);
|
||||
}, this.config.delay);
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
clearTimeout(showTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
this.hide(element);
|
||||
}, this.config.hideDelay);
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
element.addEventListener('mouseenter', showTooltip);
|
||||
element.addEventListener('mouseleave', hideTooltip);
|
||||
element.addEventListener('focus', showTooltip);
|
||||
element.addEventListener('blur', hideTooltip);
|
||||
|
||||
// Touch events (for mobile)
|
||||
element.addEventListener('touchstart', showTooltip, { passive: true });
|
||||
element.addEventListener('touchend', hideTooltip, { passive: true });
|
||||
|
||||
// Store cleanup function
|
||||
element._tooltipCleanup = () => {
|
||||
clearTimeout(showTimeout);
|
||||
clearTimeout(hideTimeout);
|
||||
element.removeEventListener('mouseenter', showTooltip);
|
||||
element.removeEventListener('mouseleave', hideTooltip);
|
||||
element.removeEventListener('focus', showTooltip);
|
||||
element.removeEventListener('blur', hideTooltip);
|
||||
element.removeEventListener('touchstart', showTooltip);
|
||||
element.removeEventListener('touchend', hideTooltip);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - Element
|
||||
* @param {string} content - Tooltip content
|
||||
* @param {string} position - Position
|
||||
* @param {Object} options - Additional options
|
||||
*/
|
||||
show(element, content, position, options = {}) {
|
||||
// Hide existing tooltip
|
||||
if (this.activeTooltip) {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
// Create tooltip element
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'livecomponent-tooltip';
|
||||
tooltip.setAttribute('role', 'tooltip');
|
||||
tooltip.setAttribute('aria-hidden', 'false');
|
||||
tooltip.textContent = content;
|
||||
|
||||
// Apply styles
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
max-width: ${this.config.maxWidth}px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
z-index: ${this.config.zIndex};
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity ${this.config.animationDuration}ms ease;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
`;
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
// Position tooltip
|
||||
this.positionTooltip(tooltip, element, position);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Store references
|
||||
this.tooltips.set(element, tooltip);
|
||||
this.activeTooltip = tooltip;
|
||||
|
||||
// Set aria-describedby on element
|
||||
const tooltipId = `tooltip-${Date.now()}`;
|
||||
tooltip.id = tooltipId;
|
||||
element.setAttribute('aria-describedby', tooltipId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position tooltip relative to element
|
||||
*
|
||||
* @param {HTMLElement} tooltip - Tooltip element
|
||||
* @param {HTMLElement} element - Target element
|
||||
* @param {string} position - Position (top, bottom, left, right)
|
||||
*/
|
||||
positionTooltip(tooltip, element, position) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const scrollX = window.scrollX || window.pageXOffset;
|
||||
const scrollY = window.scrollY || window.pageYOffset;
|
||||
const offset = this.config.offset;
|
||||
|
||||
let top, left;
|
||||
|
||||
switch (position) {
|
||||
case 'top':
|
||||
top = rect.top + scrollY - tooltipRect.height - offset;
|
||||
left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
break;
|
||||
case 'bottom':
|
||||
top = rect.bottom + scrollY + offset;
|
||||
left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
break;
|
||||
case 'left':
|
||||
top = rect.top + scrollY + (rect.height / 2) - (tooltipRect.height / 2);
|
||||
left = rect.left + scrollX - tooltipRect.width - offset;
|
||||
break;
|
||||
case 'right':
|
||||
top = rect.top + scrollY + (rect.height / 2) - (tooltipRect.height / 2);
|
||||
left = rect.right + scrollX + offset;
|
||||
break;
|
||||
default:
|
||||
top = rect.top + scrollY - tooltipRect.height - offset;
|
||||
left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
}
|
||||
|
||||
// Keep tooltip within viewport
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Horizontal adjustment
|
||||
if (left < scrollX) {
|
||||
left = scrollX + 8;
|
||||
} else if (left + tooltipRect.width > scrollX + viewportWidth) {
|
||||
left = scrollX + viewportWidth - tooltipRect.width - 8;
|
||||
}
|
||||
|
||||
// Vertical adjustment
|
||||
if (top < scrollY) {
|
||||
top = scrollY + 8;
|
||||
} else if (top + tooltipRect.height > scrollY + viewportHeight) {
|
||||
top = scrollY + viewportHeight - tooltipRect.height - 8;
|
||||
}
|
||||
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.style.left = `${left}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - Element (optional, hides active tooltip if not provided)
|
||||
*/
|
||||
hide(element = null) {
|
||||
const tooltip = element ? this.tooltips.get(element) : this.activeTooltip;
|
||||
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate out
|
||||
tooltip.style.opacity = '0';
|
||||
|
||||
// Remove after animation
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
|
||||
// Remove aria-describedby
|
||||
if (element) {
|
||||
element.removeAttribute('aria-describedby');
|
||||
}
|
||||
|
||||
// Clean up references
|
||||
if (element) {
|
||||
this.tooltips.delete(element);
|
||||
}
|
||||
if (this.activeTooltip === tooltip) {
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}, this.config.animationDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show validation error tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - Form element
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
showValidationError(element, message) {
|
||||
// Remove existing error tooltip
|
||||
this.hideValidationError(element);
|
||||
|
||||
// Show error tooltip with error styling
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'livecomponent-tooltip livecomponent-tooltip--error';
|
||||
tooltip.setAttribute('role', 'alert');
|
||||
tooltip.textContent = message;
|
||||
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
max-width: ${this.config.maxWidth}px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
z-index: ${this.config.zIndex};
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity ${this.config.animationDuration}ms ease;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
`;
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
this.positionTooltip(tooltip, element, 'top');
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Store reference
|
||||
element._validationTooltip = tooltip;
|
||||
|
||||
// Add error class to element
|
||||
element.classList.add('livecomponent-error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide validation error tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - Form element
|
||||
*/
|
||||
hideValidationError(element) {
|
||||
const tooltip = element._validationTooltip;
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate out
|
||||
tooltip.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
delete element._validationTooltip;
|
||||
element.classList.remove('livecomponent-error');
|
||||
}, this.config.animationDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all tooltips in component
|
||||
*
|
||||
* @param {HTMLElement} container - Component container
|
||||
*/
|
||||
initComponent(container) {
|
||||
// Find all elements with tooltip attributes
|
||||
const elements = container.querySelectorAll('[data-tooltip], [title], [aria-label]');
|
||||
elements.forEach(element => {
|
||||
this.init(element);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup tooltips for component
|
||||
*
|
||||
* @param {HTMLElement} container - Component container
|
||||
*/
|
||||
cleanupComponent(container) {
|
||||
const elements = container.querySelectorAll('[data-tooltip], [title], [aria-label]');
|
||||
elements.forEach(element => {
|
||||
// Hide tooltip
|
||||
this.hide(element);
|
||||
|
||||
// Cleanup event listeners
|
||||
if (element._tooltipCleanup) {
|
||||
element._tooltipCleanup();
|
||||
delete element._tooltipCleanup;
|
||||
}
|
||||
|
||||
// Restore original title
|
||||
if (element.dataset.originalTitle) {
|
||||
element.setAttribute('title', element.dataset.originalTitle);
|
||||
delete element.dataset.originalTitle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*
|
||||
* @param {Object} newConfig - New configuration
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const tooltipManager = new TooltipManager();
|
||||
|
||||
Reference in New Issue
Block a user