Files
recipe-mockup/js/ui/calendarPopover.js
ulfrxdev 08a275093c
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m16s
Unify calendar code
2026-04-20 23:44:18 +02:00

168 lines
4.9 KiB
JavaScript

const DEFAULT_POPOVER_CLASS = 'absolute left-0 right-0 top-full mt-2 z-[50] transition-all duration-200 pointer-events-none';
const DEFAULT_POPOVER_STYLE = 'opacity:0; transform:translateY(-6px) scale(0.98);';
const DEFAULT_PANEL_CLASS = 'rounded-[1.35rem] py-3';
const DEFAULT_PANEL_STYLE = 'background:rgb(var(--sunken-rgb)); border:1px solid rgb(var(--border-input-rgb)); box-shadow:var(--shadow-shell);';
const DEFAULT_OPEN_TRIGGER_STYLE = {
background: 'rgb(var(--sunken-rgb))',
borderColor: 'rgb(var(--border-input-rgb))',
};
const DEFAULT_CLOSED_TRIGGER_STYLE = {
background: 'rgb(var(--card-rgb))',
borderColor: 'rgb(var(--border-card-rgb))',
};
function byId(idOrElement) {
if (!idOrElement) return null;
if (typeof idOrElement === 'string') return document.getElementById(idOrElement);
return idOrElement;
}
function setStyles(el, styles = {}, important = false) {
if (!el) return;
Object.entries(styles).forEach(([name, value]) => {
if (value == null) return;
if (important) {
const cssName = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
el.style.setProperty(cssName, value, 'important');
}
else el.style[name] = value;
});
}
export function createCalendarPopoverHTML({
id,
calendarHTML,
popoverClass = DEFAULT_POPOVER_CLASS,
popoverStyle = DEFAULT_POPOVER_STYLE,
panelClass = DEFAULT_PANEL_CLASS,
panelStyle = DEFAULT_PANEL_STYLE,
wrapInPanel = true,
}) {
const body = wrapInPanel
? `<div class="${panelClass}" style="${panelStyle}">${calendarHTML}</div>`
: calendarHTML;
return `
<div id="${id}" class="${popoverClass}" style="${popoverStyle}">
${body}
</div>
`;
}
export function syncCalendarPopoverVisibility({
popup,
isOpen,
chevron,
trigger,
openTriggerStyle = DEFAULT_OPEN_TRIGGER_STYLE,
closedTriggerStyle = DEFAULT_CLOSED_TRIGGER_STYLE,
triggerImportant = false,
}) {
const popupEl = byId(popup);
if (popupEl) {
popupEl.style.opacity = isOpen ? '1' : '0';
popupEl.style.transform = isOpen ? 'translateY(0) scale(1)' : 'translateY(-6px) scale(0.98)';
popupEl.style.pointerEvents = isOpen ? 'auto' : 'none';
}
const chevronEl = byId(chevron);
if (chevronEl) chevronEl.style.transform = isOpen ? 'rotate(180deg)' : 'rotate(0deg)';
setStyles(
byId(trigger),
isOpen ? openTriggerStyle : closedTriggerStyle,
triggerImportant,
);
}
export function stabilizeSwipeCalendarLayout({
calendar,
viewport,
hideViewport = false,
maxAttempts = 8,
}) {
const viewportEl = byId(viewport);
if (hideViewport && viewportEl) {
viewportEl.style.opacity = '0';
viewportEl.style.visibility = 'hidden';
viewportEl.style.transition = 'opacity 120ms ease';
}
calendar?.render?.();
const ensureStableLayout = (attempt = 0) => {
const width = viewportEl ? (viewportEl.clientWidth || viewportEl.getBoundingClientRect().width) : 0;
if (viewportEl && width < 8 && attempt < maxAttempts) {
requestAnimationFrame(() => ensureStableLayout(attempt + 1));
return;
}
calendar?.reapplyLayout?.();
calendar?.resetTrackPosition?.();
requestAnimationFrame(() => {
calendar?.reapplyLayout?.();
calendar?.resetTrackPosition?.();
if (hideViewport && viewportEl) {
viewportEl.style.visibility = 'visible';
viewportEl.style.opacity = '1';
}
});
};
requestAnimationFrame(() => ensureStableLayout());
}
export function createCalendarPopoverController({
popupId,
viewportId,
triggerId,
chevronId,
getCalendar,
openTriggerStyle = DEFAULT_OPEN_TRIGGER_STYLE,
closedTriggerStyle = DEFAULT_CLOSED_TRIGGER_STYLE,
triggerImportant = false,
hideViewportDuringLayout = false,
}) {
const calendar = () => (typeof getCalendar === 'function' ? getCalendar() : null);
const sync = (isOpen) => {
syncCalendarPopoverVisibility({
popup: popupId,
isOpen,
chevron: chevronId,
trigger: triggerId,
openTriggerStyle,
closedTriggerStyle,
triggerImportant,
});
};
const stabilize = () => {
stabilizeSwipeCalendarLayout({
calendar: calendar(),
viewport: viewportId,
hideViewport: hideViewportDuringLayout,
});
};
const close = ({ clearPendingRange = false } = {}) => {
sync(false);
const instance = calendar();
if (clearPendingRange) instance?.clearPendingRange?.();
instance?.resetTrackPosition?.();
};
const open = () => {
sync(true);
stabilize();
};
return {
sync,
open,
close,
stabilize,
};
}