fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
346
resources/js/modules/livecomponent/PopoverManager.js
Normal file
346
resources/js/modules/livecomponent/PopoverManager.js
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Popover Manager for LiveComponents
|
||||
*
|
||||
* Manages popover components with:
|
||||
* - Native Popover API support (Chrome 114+)
|
||||
* - Fallback for older browsers
|
||||
* - Position calculation relative to anchor
|
||||
* - Auto-repositioning on scroll/resize
|
||||
* - Light-dismiss handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if Popover API is supported
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function supportsPopoverAPI() {
|
||||
// Check for popover property
|
||||
if (HTMLElement.prototype.hasOwnProperty('popover')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for showPopover method
|
||||
if ('showPopover' in HTMLElement.prototype) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for CSS @supports (only in browser environment)
|
||||
if (typeof CSS !== 'undefined' && CSS.supports) {
|
||||
try {
|
||||
return CSS.supports('popover', 'auto');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export class PopoverManager {
|
||||
constructor() {
|
||||
this.popovers = new Map(); // componentId → popover element
|
||||
this.usePopoverAPI = supportsPopoverAPI();
|
||||
this.resizeHandler = null;
|
||||
this.scrollHandler = null;
|
||||
|
||||
if (this.usePopoverAPI) {
|
||||
console.log('[PopoverManager] Using native Popover API');
|
||||
} else {
|
||||
console.log('[PopoverManager] Using fallback implementation');
|
||||
this.setupGlobalHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popover
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {Object} options - Popover options
|
||||
* @returns {Object} Popover instance
|
||||
*/
|
||||
show(componentId, options = {}) {
|
||||
const {
|
||||
content = '',
|
||||
title = '',
|
||||
anchorId = null,
|
||||
position = 'top',
|
||||
showArrow = true,
|
||||
offset = 8,
|
||||
closeOnOutsideClick = true,
|
||||
zIndex = 1060
|
||||
} = options;
|
||||
|
||||
if (!anchorId) {
|
||||
console.warn('[PopoverManager] Popover requires anchorId');
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchor = document.getElementById(anchorId);
|
||||
if (!anchor) {
|
||||
console.warn(`[PopoverManager] Anchor element not found: ${anchorId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove existing popover for this component
|
||||
this.hide(componentId);
|
||||
|
||||
if (this.usePopoverAPI) {
|
||||
return this.showWithPopoverAPI(componentId, anchor, content, title, position, showArrow, offset, zIndex);
|
||||
} else {
|
||||
return this.showWithFallback(componentId, anchor, content, title, position, showArrow, offset, closeOnOutsideClick, zIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popover using native Popover API
|
||||
*/
|
||||
showWithPopoverAPI(componentId, anchor, content, title, position, showArrow, offset, zIndex) {
|
||||
const popover = document.createElement('div');
|
||||
popover.setAttribute('popover', 'auto'); // 'auto' = light-dismiss enabled
|
||||
popover.className = `livecomponent-popover popover--${position}`;
|
||||
popover.id = `popover-${componentId}`;
|
||||
popover.style.zIndex = zIndex.toString();
|
||||
|
||||
popover.innerHTML = `
|
||||
${showArrow ? `<div class="popover-arrow popover-arrow--${position}"></div>` : ''}
|
||||
${title ? `<div class="popover-header"><h4>${title}</h4></div>` : ''}
|
||||
<div class="popover-body">${content}</div>
|
||||
`;
|
||||
|
||||
// Set anchor using anchor attribute (when supported)
|
||||
if (popover.setPopoverAnchor) {
|
||||
popover.setPopoverAnchor(anchor);
|
||||
} else {
|
||||
// Fallback: position manually
|
||||
this.positionPopover(popover, anchor, position, offset);
|
||||
}
|
||||
|
||||
document.body.appendChild(popover);
|
||||
popover.showPopover();
|
||||
|
||||
this.popovers.set(componentId, popover);
|
||||
|
||||
// Setup repositioning on scroll/resize
|
||||
this.setupRepositioning(popover, anchor, position, offset);
|
||||
|
||||
return {
|
||||
element: popover,
|
||||
hide: () => this.hide(componentId),
|
||||
isVisible: () => popover.matches(':popover-open')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popover using fallback implementation
|
||||
*/
|
||||
showWithFallback(componentId, anchor, content, title, position, showArrow, offset, closeOnOutsideClick, zIndex) {
|
||||
const popover = document.createElement('div');
|
||||
popover.className = `livecomponent-popover popover-fallback popover--${position}`;
|
||||
popover.id = `popover-${componentId}`;
|
||||
popover.style.zIndex = zIndex.toString();
|
||||
popover.setAttribute('role', 'tooltip');
|
||||
popover.setAttribute('aria-hidden', 'false');
|
||||
|
||||
popover.innerHTML = `
|
||||
${showArrow ? `<div class="popover-arrow popover-arrow--${position}"></div>` : ''}
|
||||
${title ? `<div class="popover-header"><h4>${title}</h4></div>` : ''}
|
||||
<div class="popover-body">${content}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(popover);
|
||||
|
||||
// Position popover
|
||||
this.positionPopover(popover, anchor, position, offset);
|
||||
|
||||
// Show popover
|
||||
popover.style.display = 'block';
|
||||
popover.classList.add('popover--show');
|
||||
|
||||
this.popovers.set(componentId, popover);
|
||||
|
||||
// Setup click-outside handling
|
||||
if (closeOnOutsideClick) {
|
||||
const clickHandler = (e) => {
|
||||
if (!popover.contains(e.target) && !anchor.contains(e.target)) {
|
||||
this.hide(componentId);
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', clickHandler);
|
||||
popover._clickHandler = clickHandler;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Setup repositioning on scroll/resize
|
||||
this.setupRepositioning(popover, anchor, position, offset);
|
||||
|
||||
return {
|
||||
element: popover,
|
||||
hide: () => this.hide(componentId),
|
||||
isVisible: () => popover.classList.contains('popover--show')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide popover
|
||||
* @param {string} componentId - Component ID
|
||||
*/
|
||||
hide(componentId) {
|
||||
const popover = this.popovers.get(componentId);
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.usePopoverAPI) {
|
||||
if (popover.hidePopover) {
|
||||
popover.hidePopover();
|
||||
}
|
||||
} else {
|
||||
popover.classList.remove('popover--show');
|
||||
popover.style.display = 'none';
|
||||
|
||||
// Remove click handler
|
||||
if (popover._clickHandler) {
|
||||
document.removeEventListener('click', popover._clickHandler);
|
||||
delete popover._clickHandler;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from DOM
|
||||
if (popover.parentNode) {
|
||||
popover.parentNode.removeChild(popover);
|
||||
}
|
||||
|
||||
this.popovers.delete(componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position popover relative to anchor
|
||||
* @param {HTMLElement} popover - Popover element
|
||||
* @param {HTMLElement} anchor - Anchor element
|
||||
* @param {string} position - Position (top, bottom, left, right)
|
||||
* @param {number} offset - Offset in pixels
|
||||
*/
|
||||
positionPopover(popover, anchor, position, offset) {
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const popoverRect = popover.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (position) {
|
||||
case 'top':
|
||||
top = anchorRect.top - popoverRect.height - offset;
|
||||
left = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
|
||||
break;
|
||||
case 'bottom':
|
||||
top = anchorRect.bottom + offset;
|
||||
left = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
|
||||
break;
|
||||
case 'left':
|
||||
top = anchorRect.top + (anchorRect.height / 2) - (popoverRect.height / 2);
|
||||
left = anchorRect.left - popoverRect.width - offset;
|
||||
break;
|
||||
case 'right':
|
||||
top = anchorRect.top + (anchorRect.height / 2) - (popoverRect.height / 2);
|
||||
left = anchorRect.right + offset;
|
||||
break;
|
||||
case 'auto':
|
||||
// Auto-position: choose best position based on viewport
|
||||
const positions = ['bottom', 'top', 'right', 'left'];
|
||||
for (const pos of positions) {
|
||||
const testPos = this.calculatePosition(anchorRect, popoverRect, pos, offset);
|
||||
if (this.isPositionValid(testPos, popoverRect, viewportWidth, viewportHeight)) {
|
||||
position = pos;
|
||||
({ top, left } = testPos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure popover stays within viewport
|
||||
left = Math.max(8, Math.min(left, viewportWidth - popoverRect.width - 8));
|
||||
top = Math.max(8, Math.min(top, viewportHeight - popoverRect.height - 8));
|
||||
|
||||
popover.style.position = 'fixed';
|
||||
popover.style.top = `${top}px`;
|
||||
popover.style.left = `${left}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position for a given direction
|
||||
*/
|
||||
calculatePosition(anchorRect, popoverRect, position, offset) {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (position) {
|
||||
case 'top':
|
||||
top = anchorRect.top - popoverRect.height - offset;
|
||||
left = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
|
||||
break;
|
||||
case 'bottom':
|
||||
top = anchorRect.bottom + offset;
|
||||
left = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
|
||||
break;
|
||||
case 'left':
|
||||
top = anchorRect.top + (anchorRect.height / 2) - (popoverRect.height / 2);
|
||||
left = anchorRect.left - popoverRect.width - offset;
|
||||
break;
|
||||
case 'right':
|
||||
top = anchorRect.top + (anchorRect.height / 2) - (popoverRect.height / 2);
|
||||
left = anchorRect.right + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
return { top, left };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position is valid (within viewport)
|
||||
*/
|
||||
isPositionValid({ top, left }, popoverRect, viewportWidth, viewportHeight) {
|
||||
return top >= 0 &&
|
||||
left >= 0 &&
|
||||
top + popoverRect.height <= viewportHeight &&
|
||||
left + popoverRect.width <= viewportWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup repositioning on scroll/resize
|
||||
*/
|
||||
setupRepositioning(popover, anchor, position, offset) {
|
||||
const reposition = () => {
|
||||
if (this.popovers.has(popover.id.replace('popover-', ''))) {
|
||||
this.positionPopover(popover, anchor, position, offset);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', reposition, { passive: true });
|
||||
window.addEventListener('resize', reposition);
|
||||
|
||||
popover._repositionHandlers = {
|
||||
scroll: reposition,
|
||||
resize: reposition
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup global handlers for fallback mode
|
||||
*/
|
||||
setupGlobalHandlers() {
|
||||
// Global handlers are set up per-popover in showWithFallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all popovers
|
||||
*/
|
||||
destroy() {
|
||||
for (const componentId of this.popovers.keys()) {
|
||||
this.hide(componentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user