123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- import { type ObjectDirective } from 'vue'
- import { useAppName } from '#shared/composables/useAppName.ts'
- import { useLocaleStore } from '#shared/stores/locale.ts'
- interface Modifiers {
- truncate?: boolean
- }
- let isListeningToEvents = false
- let isTooltipInDom = false
- let hasHoverOnNode = false
- let currentEvent: MouseEvent | TouchEvent | null = null
- let tooltipTimeout: NodeJS.Timeout | null = null
- let tooltipRecordsCount = 0
- let tooltipTargetRecords: WeakMap<HTMLElement, { modifiers: Modifiers }> =
- new WeakMap()
- const removeTooltips = () => {
- document
- .querySelectorAll('[role="tooltip"]')
- .forEach((node) => node?.remove())
- isTooltipInDom = false
- }
- const addModifierRecord = (element: HTMLDivElement, modifiers: Modifiers) => {
- if (tooltipTargetRecords.has(element)) return
- tooltipRecordsCount += 1
- tooltipTargetRecords.set(element, {
- modifiers,
- })
- }
- const removeModifierRecord = (element: HTMLDivElement) => {
- if (!tooltipTargetRecords.has(element)) return
- tooltipRecordsCount -= 1
- tooltipTargetRecords.delete(element)
- }
- const getModifierRecord = ($el: HTMLDivElement) => {
- return tooltipTargetRecords.get($el) || null
- }
- const createTooltip = (
- { top, left }: { top: string; left: string },
- message: string,
- ) => {
- const tooltipNode = document.createElement('div')
- tooltipNode.classList.add('tooltip')
- tooltipNode.style.top = top
- tooltipNode.style.left = left
- tooltipNode.setAttribute('aria-hidden', 'true')
- tooltipNode.setAttribute('role', 'tooltip')
-
- const availableWidth = window.innerWidth / 2
- tooltipNode.style.maxWidth = `${availableWidth}px`
- const tooltipMessageNode = document.createElement('p')
- tooltipMessageNode.textContent = message
- tooltipNode.insertAdjacentElement('afterbegin', tooltipMessageNode)
- return tooltipNode
- }
- const getLeftBasedOnLanguage = (clientX: number, tooltipRectangle: DOMRect) => {
- const isRTL = useLocaleStore().localeData?.dir === 'rtl'
- let left = ''
- if (isRTL) {
-
- const leftValue = clientX - tooltipRectangle.width
- left = `${leftValue}px`
-
- if (leftValue < 0) {
-
- left = '0px'
- }
- } else {
-
- left = `${clientX}px`
-
- if (clientX + tooltipRectangle.width > window.innerWidth) {
-
- left = `${window.innerWidth - tooltipRectangle.width}px`
- }
- }
- return left
- }
- const addTooltip = (
- targetNode: HTMLDivElement,
- message: string,
- {
- event,
- }: {
- event: MouseEvent | TouchEvent
- },
- ) => {
- if (!event) return
- const tooltipNode = createTooltip({ top: '0px', left: '0px' }, message)
- document.body.appendChild(tooltipNode)
- const tooltipRectangle = tooltipNode.getBoundingClientRect()
- let top: string
- let left: string
- if (!event) return
- if ('touches' in event) {
- const { clientX, clientY } = event.touches[0]
- top = `${clientY}px`
- left = getLeftBasedOnLanguage(clientX, tooltipRectangle)
- } else {
- const { clientX, clientY } = event
- const verticalThreshold = 10
- const thresholdToBottom = 30
- const availableSpaceBelow = window.innerHeight - clientY - thresholdToBottom
-
- if (availableSpaceBelow < tooltipRectangle.height) {
- top = `${clientY - verticalThreshold - tooltipRectangle.height}px`
- } else {
- top = `${clientY + verticalThreshold}px`
- }
- left = getLeftBasedOnLanguage(clientX, tooltipRectangle)
- }
- tooltipNode.style.top = top
- tooltipNode.style.left = left
- document.body.insertAdjacentElement('beforeend', tooltipNode)
- setTimeout(() => {
- tooltipNode.classList.add('tooltip-animate')
- }, 500)
- }
- const isContentTruncated = (element: HTMLElement) => {
- const { parentElement } = element
-
- if (!parentElement) return element.offsetWidth < element.scrollWidth
- return parentElement.offsetWidth < parentElement.scrollWidth
- }
- const evaluateModifiers = (element: HTMLElement, options?: Modifiers) => {
- const modifications = {
- isTruncated: false,
- top: false,
- }
- if (options?.truncate) {
- modifications.isTruncated = isContentTruncated(element)
- }
- return modifications
- }
- const findTooltipTarget = (
- element: HTMLDivElement | null,
- ): HTMLDivElement | null => element?.closest('[data-tooltip]') || null
- const handleTooltipAddEvent = (event: MouseEvent | TouchEvent) => {
- if (isTooltipInDom) removeTooltips()
- if (!event.target) return
-
- const tooltipTargetNode = findTooltipTarget(event.target as HTMLDivElement)
- if (!tooltipTargetNode) return
-
- const message = tooltipTargetNode.getAttribute('aria-label')
- if (!message) return
-
-
- if (tooltipTargetNode.closest('.no-tooltip')) return
- hasHoverOnNode = true
- const tooltipRecord = getModifierRecord(tooltipTargetNode)
- const { isTruncated } = evaluateModifiers(
- tooltipTargetNode,
- tooltipRecord?.modifiers,
- )
-
- if (!isTruncated && tooltipRecord?.modifiers.truncate) return
- if (tooltipTimeout) clearTimeout(tooltipTimeout)
- tooltipTimeout = setTimeout(() => {
- addTooltip(tooltipTargetNode, message as string, {
- event: currentEvent as MouseEvent,
- })
- isTooltipInDom = true
- }, 300)
- }
- const handleEvent = (event: MouseEvent | TouchEvent) => {
- if (hasHoverOnNode) currentEvent = event
- }
- const handleTooltipRemoveEvent = () => {
- if (tooltipTimeout) clearTimeout(tooltipTimeout)
- if (isTooltipInDom) removeTooltips()
- }
- const addEventListeners = () => {
- window.addEventListener('scroll', handleTooltipRemoveEvent, {
- passive: true,
- capture: true,
- })
- window.addEventListener('touchstart', handleTooltipAddEvent, {
- passive: true,
- })
- window.addEventListener('touchmove', handleEvent, {
- passive: true,
- })
- window.addEventListener('touchcancel', handleTooltipRemoveEvent, {
- passive: true,
- })
- window.addEventListener('mouseover', handleTooltipAddEvent, {
- passive: true,
- })
- window.addEventListener('mousemove', handleEvent, {
- passive: true,
- })
- window.addEventListener('mouseout', handleTooltipRemoveEvent, {
- passive: true,
- })
- }
- const cleanupEventHandlers = () => {
- window.removeEventListener('touchstart', handleTooltipAddEvent)
- window.removeEventListener('touchmove', handleEvent)
- window.removeEventListener('touchcancel', handleTooltipRemoveEvent)
- window.removeEventListener('mouseover', handleTooltipAddEvent)
- window.removeEventListener('mousemove', handleEvent)
- window.removeEventListener('mouseout', handleTooltipRemoveEvent)
- window.removeEventListener('scroll', handleTooltipRemoveEvent)
- }
- const cleanupAndAddEventListeners = () => {
- cleanupEventHandlers()
- addEventListeners()
- }
- export default {
- name: 'tooltip',
- directive: {
- mounted: (element: HTMLDivElement, { value: message, modifiers }) => {
- if (!message) return
- element.setAttribute('aria-label', message)
-
- if (useAppName() === 'mobile') return
- element.setAttribute('data-tooltip', 'true')
- addModifierRecord(element, modifiers)
- if (!isListeningToEvents) {
- addEventListeners()
- isListeningToEvents = true
-
- window.addEventListener('resize', cleanupAndAddEventListeners)
- }
- },
- updated(element: HTMLDivElement, { value: message }) {
- if (!message) {
- if (element.getAttribute('aria-label'))
- element.removeAttribute('aria-label')
- return
- }
-
-
- if (element.getAttribute('aria-label') !== message)
- element.setAttribute('aria-label', message)
- },
- beforeUnmount(element) {
-
- removeModifierRecord(element)
-
- if (tooltipRecordsCount !== 1) return
-
- if (isTooltipInDom) removeTooltips()
- if (isListeningToEvents) cleanupEventHandlers()
- isListeningToEvents = false
- tooltipTargetRecords = new WeakMap()
- tooltipRecordsCount = 0
- window.removeEventListener('resize', cleanupAndAddEventListeners)
- },
- },
- } as {
- name: string
- directive: ObjectDirective
- }
|