UNPKG

svooltip

Version:

A basic Svelte tooltip directive. Powered by [Floating UI](https://floating-ui.com/).

146 lines (145 loc) 5.91 kB
import { computePosition, flip as floatingFlip, shift as floatingShift, offset as floatingOffset, arrow as floatingArrow, autoUpdate } from '@floating-ui/dom'; import { animate, wait, ID } from './utils.js'; import { DEFAULTS } from './defaults.js'; export default (node, options) => { let Config = { ...DEFAULTS, ...options }; let currentDelay; let content = node.title || Config.content; let visible = false; let hovering = false; let wasDestroyed = false; let cleanUpPosition = null; let Tooltip = null; let TooltipContent = null; let TooltipArrow = null; const targetElement = typeof Config.target === 'string' ? document.querySelector(Config.target) : Config.target; const parseDelay = { in: typeof Config.delay === 'number' ? Config.delay : Config.delay[0], out: typeof Config.delay === 'number' ? Config.delay : Config.delay[1] }; const UID = `svooltip-${ID()}`; if (node.title) node.removeAttribute('title'); const handleKeys = (e) => { if (e.key === 'Escape') removeTooltip(); }; const createTooltip = () => { if (Tooltip || visible) return; Tooltip = document.createElement('div'); Tooltip.setAttribute('id', UID); Tooltip.setAttribute('role', 'tooltip'); Tooltip.setAttribute('data-placement', Config.placement); Tooltip.setAttribute('class', Config.classes.container); TooltipContent = document.createElement('span'); TooltipContent.setAttribute('class', Config.classes.content); TooltipContent[Config.html ? 'innerHTML' : 'textContent'] = content; TooltipArrow = document.createElement('div'); TooltipArrow.setAttribute('class', Config.classes.arrow); Tooltip.append(TooltipArrow); Tooltip.append(TooltipContent); if (!targetElement) { document.body.append(Tooltip); } else { targetElement.append(Tooltip); } }; const mountTooltip = async () => { if (!Tooltip && Config.visibility) { if (parseDelay.in) { await wait(parseDelay.in, currentDelay); if (wasDestroyed || !hovering || visible || Tooltip) return; } node.setAttribute('aria-describedby', UID); createTooltip(); if (!Tooltip) throw new Error(`[SVooltip] Tooltip has not been created.`); cleanUpPosition = autoUpdate(node, Tooltip, () => { if (!Tooltip || !TooltipArrow) return; computePosition(node, Tooltip, { placement: Config.placement, middleware: [ floatingOffset(Config.offset), floatingFlip(), floatingShift({ padding: Config.shiftPadding }), floatingArrow({ element: TooltipArrow }), ...Config.middleware ] }).then(({ x, y, placement, middlewareData }) => { Tooltip.style.left = `${x}px`; Tooltip.style.top = `${y}px`; const { x: arrowX, y: arrowY } = middlewareData.arrow; const arrowSize = (TooltipArrow.getBoundingClientRect().width / 3).toFixed(); const side = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]; Object.assign(TooltipArrow.style, { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', [side]: `-${arrowSize}px` }); Tooltip?.setAttribute('data-placement', placement); }); }); await animate(Config.classes.animationEnter, Config.classes.animationLeave, Tooltip); visible = true; Config.onMount?.(); } }; const removeTooltip = async () => { if (Tooltip || visible) { if (parseDelay.out) { await wait(parseDelay.out, currentDelay); } await animate(Config.classes.animationLeave, Config.classes.animationEnter, Tooltip); if (cleanUpPosition) cleanUpPosition(); if (Tooltip) { node.removeAttribute('aria-describedby'); visible = false; Tooltip.remove(); Tooltip = null; Config.onDestroy?.(); } cleanUpPosition?.(); } }; if (Config.constant) { mountTooltip(); } else { node.addEventListener('mouseenter', mountTooltip); node.addEventListener('mouseenter', () => (hovering = true)); node.addEventListener('focus', mountTooltip); node.addEventListener('mouseleave', removeTooltip); node.addEventListener('mouseleave', () => (hovering = false)); node.addEventListener('blur', removeTooltip); window.addEventListener('keydown', handleKeys); return { update(options) { content = options.content; Config.html = options.html || false; if (Tooltip && TooltipContent) { TooltipContent[Config.html ? 'innerHTML' : 'textContent'] = content; } }, destroy() { removeTooltip(); window.removeEventListener('keydown', handleKeys); wasDestroyed = true; } }; } };