import type { TemplateRef } from 'vue'; import { useFloating, type UseFloatingOptions } from '@floating-ui/vue'; export function usePopup( containerElement: TemplateRef, toggleElement: TemplateRef, popupElement: TemplateRef, options?: UseFloatingOptions, ) { const showDelay = 400; const popupVisible = ref(false); const showTimeout = ref>(); const { floatingStyles: popupStyles } = useFloating( containerElement, popupElement, options, ); function showPopup() { showTimeout.value = setTimeout(() => { popupVisible.value = true; }, showDelay); } function hidePopup() { if (showTimeout.value) { clearTimeout(showTimeout.value); } disableScrollBreaker(); disableContextMenuBreaker(); disableOutsideTouchCloser(); popupVisible.value = false; } const contextMenuBreaker = (e: Event) => { e.preventDefault(); e.stopPropagation(); return false; }; function enableContextMenuBreaker() { if (toggleElement.value) { toggleElement.value.addEventListener('contextmenu', contextMenuBreaker); } } function disableContextMenuBreaker() { if (toggleElement.value) { toggleElement.value.removeEventListener( 'contextmenu', contextMenuBreaker, ); } } const scrollBreaker = () => { // Break showing wait if scrolling occurs if (showTimeout.value) { clearTimeout(showTimeout.value); } // But do not hide already showing popup. // It is okay to scroll after the popup is already shown. }; function enableScrollBreaker() { addEventListener('scroll', scrollBreaker); addEventListener('resize', scrollBreaker); } function disableScrollBreaker() { removeEventListener('scroll', scrollBreaker); removeEventListener('resize', scrollBreaker); } const outsideTouchCloser = (e: TouchEvent) => { if (!popupVisible.value) { return; } const container = containerElement.value; if (!container) { return; } const target = e.target as Node | null; if (target && container.contains(target)) { return; } hidePopup(); }; function enableOutsideTouchCloser() { addEventListener('touchstart', outsideTouchCloser, true); } function disableOutsideTouchCloser() { removeEventListener('touchstart', outsideTouchCloser, true); } onMounted(() => { if (containerElement.value) { containerElement.value.addEventListener('mouseenter', showPopup); containerElement.value.addEventListener('mouseleave', hidePopup); } if (toggleElement.value) { toggleElement.value.addEventListener('touchstart', (e) => { if (popupVisible.value) { // Want to hide hidePopup(); } else { // Want to show enableScrollBreaker(); enableContextMenuBreaker(); enableOutsideTouchCloser(); showPopup(); } }); toggleElement.value.addEventListener('touchend', (e) => { if (showTimeout.value) { clearTimeout(showTimeout.value); } }); } }); onUnmounted(() => { if (showTimeout.value) { clearTimeout(showTimeout.value); } disableOutsideTouchCloser(); }); return { popupVisible, popupStyles, showPopup, hidePopup, }; }