diff --git a/resources/js/bootstrap/statamic.js b/resources/js/bootstrap/statamic.js index fc0fd03c44..4bfd496b7f 100644 --- a/resources/js/bootstrap/statamic.js +++ b/resources/js/bootstrap/statamic.js @@ -7,8 +7,8 @@ import registerGlobalCommandPalette from './commands.js'; import registerUiComponents from './ui.js'; import registerFieldtypes from './fieldtypes.js'; import VueClickAway from 'vue3-click-away'; -import FloatingVue from 'floating-vue'; import 'floating-vue/dist/style.css'; +import tooltipDirective from '@/directives/tooltip.js'; import { createInertiaApp } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3'; import PortalVue from 'portal-vue'; @@ -244,7 +244,7 @@ export default { this.$app.use(createPinia()); this.$app.use(PortalVue, { portalName: 'v-portal' }); this.$app.use(VueClickAway); - this.$app.use(FloatingVue, { disposeTimeout: 30000, distance: 10 }); + this.$app.directive('tooltip', tooltipDirective); this.$app.use(VueComponentDebug, { enabled: import.meta.env.VITE_VUE_COMPONENT_DEBUG === 'true' }); toast.initialize(this.$app); diff --git a/resources/js/components/Tooltips.vue b/resources/js/components/Tooltips.vue new file mode 100644 index 0000000000..8a522fadf0 --- /dev/null +++ b/resources/js/components/Tooltips.vue @@ -0,0 +1,74 @@ + + + diff --git a/resources/js/composables/tooltip.js b/resources/js/composables/tooltip.js new file mode 100644 index 0000000000..2b5c1778da --- /dev/null +++ b/resources/js/composables/tooltip.js @@ -0,0 +1,74 @@ +import { ref, shallowRef, readonly } from 'vue'; + +const isVisible = ref(false); +const content = ref(''); +const html = ref(false); +const targetEl = shallowRef(null); + +let hideTimeout = null; +let showTimeout = null; + +function setContent(el, options) { + targetEl.value = el; + + if (typeof options === 'string') { + content.value = options; + html.value = false; + } else if (options && typeof options === 'object') { + content.value = options.content || ''; + html.value = options.html || false; + } else { + content.value = ''; + html.value = false; + } +} + +function show(el, options) { + if (hideTimeout) { + clearTimeout(hideTimeout); + hideTimeout = null; + } + + if (showTimeout) { + clearTimeout(showTimeout); + } + + // If already visible, update immediately (for moving between adjacent elements) + if (isVisible.value) { + setContent(el, options); + return; + } + + showTimeout = setTimeout(() => { + setContent(el, options); + + if (content.value) { + isVisible.value = true; + } + }, 200); +} + +function hide() { + if (showTimeout) { + clearTimeout(showTimeout); + showTimeout = null; + } + + hideTimeout = setTimeout(() => { + isVisible.value = false; + targetEl.value = null; + content.value = ''; + html.value = false; + }, 50); +} + +export function useTooltip() { + return { + isVisible: readonly(isVisible), + content: readonly(content), + html: readonly(html), + targetEl: readonly(targetEl), + show, + hide, + }; +} diff --git a/resources/js/directives/tooltip.js b/resources/js/directives/tooltip.js new file mode 100644 index 0000000000..8ae42ce4e2 --- /dev/null +++ b/resources/js/directives/tooltip.js @@ -0,0 +1,49 @@ +import { useTooltip } from '@/composables/tooltip.js'; + +const { show, hide } = useTooltip(); + +function getOptions(binding) { + const value = binding.value; + + if (value === null || value === undefined || value === false || value === '') { + return null; + } + + return value; +} + +function handleMouseEnter(el, binding) { + const options = getOptions(binding); + if (options) { + show(el, options); + } +} + +function handleMouseLeave() { + hide(); +} + +export default { + mounted(el, binding) { + el._tooltipBinding = binding; + el._tooltipMouseEnter = () => handleMouseEnter(el, el._tooltipBinding); + el._tooltipMouseLeave = handleMouseLeave; + + el.addEventListener('mouseenter', el._tooltipMouseEnter); + el.addEventListener('mouseleave', el._tooltipMouseLeave); + el.addEventListener('focus', el._tooltipMouseEnter); + el.addEventListener('blur', el._tooltipMouseLeave); + }, + + updated(el, binding) { + el._tooltipBinding = binding; + }, + + beforeUnmount(el) { + el.removeEventListener('mouseenter', el._tooltipMouseEnter); + el.removeEventListener('mouseleave', el._tooltipMouseLeave); + el.removeEventListener('focus', el._tooltipMouseEnter); + el.removeEventListener('blur', el._tooltipMouseLeave); + hide(); + }, +}; diff --git a/resources/js/pages/layout/Layout.vue b/resources/js/pages/layout/Layout.vue index e3a6e18b9d..865662d685 100644 --- a/resources/js/pages/layout/Layout.vue +++ b/resources/js/pages/layout/Layout.vue @@ -5,6 +5,7 @@ import { ConfigProvider } from 'reka-ui'; import SessionExpiry from '@/components/SessionExpiry.vue'; import LicensingAlert from '@/components/LicensingAlert.vue'; import PortalTargets from '@/components/portals/PortalTargets.vue'; +import Tooltips from '@/components/Tooltips.vue'; import { provide, watch, ref } from 'vue'; import useBodyClasses from './body-classes.js'; import useStatamicPageProps from '@/composables/page-props.js'; @@ -61,5 +62,6 @@ provide('layout', { +