UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

305 lines (241 loc) 9.66 kB
/* * HSTooltip * @version: 3.2.2 * @author: Preline Labs Ltd. * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) * Copyright 2024 Preline Labs Ltd. */ import { type Strategy, computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom' import { afterTransition, dispatch, getClassProperty } from '../../utils' import { ITooltip } from './interfaces' import { TTooltipOptionsScope } from './types' import HSBasePlugin from '../base-plugin' import { ICollectionItem } from '../../interfaces' import { POSITIONS } from '../../constants' class HSTooltip extends HSBasePlugin<{}> implements ITooltip { private readonly toggle: HTMLElement | null public content: HTMLElement | null readonly eventMode: string private readonly preventFloatingUI: string private readonly placement: string private readonly strategy: Strategy private readonly scope: TTooltipOptionsScope cleanupAutoUpdate: (() => void) | null = null private onToggleClickListener: () => void private onToggleFocusListener: () => void private onToggleBlurListener: () => void private onToggleMouseEnterListener: () => void private onToggleMouseLeaveListener: () => void private onToggleHandleListener: () => void constructor(el: HTMLElement, options?: {}, events?: {}) { super(el, options, events) if (this.el) { this.toggle = this.el.querySelector('.tooltip-toggle') || this.el this.content = this.el.querySelector('.tooltip-content') this.eventMode = getClassProperty(this.el, '--trigger') || 'hover' this.preventFloatingUI = getClassProperty(this.el, '--prevent-popper', 'false') this.placement = getClassProperty(this.el, '--placement') this.strategy = getClassProperty(this.el, '--strategy') as Strategy this.scope = (getClassProperty(this.el, '--scope') as TTooltipOptionsScope) || 'parent' } if (this.el && this.toggle && this.content) this.init() } private toggleClick() { this.click() } private toggleMouseEnter() { this.enter() } private toggleMouseLeave() { this.leave() } private toggleHandle() { this.hide() this.toggle.removeEventListener('click', this.onToggleHandleListener, true) this.toggle.removeEventListener('blur', this.onToggleHandleListener, true) } private init() { this.createCollection(window.$hsTooltipCollection, this) this.onToggleFocusListener = () => this.enter() this.onToggleBlurListener = () => this.hide() this.toggle.addEventListener('focus', this.onToggleFocusListener) this.toggle.addEventListener('blur', this.onToggleBlurListener) if (this.eventMode === 'click') { this.onToggleClickListener = () => this.toggleClick() this.toggle.addEventListener('click', this.onToggleClickListener) } else if (this.eventMode === 'hover') { this.onToggleMouseEnterListener = () => this.toggleMouseEnter() this.onToggleMouseLeaveListener = () => this.toggleMouseLeave() this.toggle.addEventListener('mouseenter', this.onToggleMouseEnterListener) this.toggle.addEventListener('mouseleave', this.onToggleMouseLeaveListener) } if (this.preventFloatingUI === 'false') this.buildFloatingUI() } private enter() { this._show() } private leave() { this.hide() } // This function is updated in flyonui. private click() { if (this.el.classList.contains('show')) { this.hide() } else { this._show() this.onToggleHandleListener = () => { setTimeout(() => this.toggleHandle()) } this.toggle.addEventListener('click', this.onToggleHandleListener, true) this.toggle.addEventListener('blur', this.onToggleHandleListener, true) } } private buildFloatingUI() { if (this.scope === 'window') document.body.appendChild(this.content) const position = POSITIONS[this.placement] ?? 'top' const verticalPositions = new Set(['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end']) const isVerticalPosition = verticalPositions.has(position) const isFloatingAllowed = this.preventFloatingUI === 'false' const shouldApplyShift = isFloatingAllowed && isVerticalPosition const shouldApplyFlip = isFloatingAllowed const middleware = [offset(0), ...(shouldApplyShift ? [shift()] : []), ...(shouldApplyFlip ? [flip()] : [])] computePosition(this.toggle, this.content, { placement: POSITIONS[this.placement] || 'top', strategy: this.strategy || 'fixed', middleware: middleware }).then(({ x, y }: { x: number; y: number }) => { Object.assign(this.content.style, { position: this.strategy || 'fixed', left: `${x}px`, top: `${y}px` }) }) this.cleanupAutoUpdate = autoUpdate(this.toggle, this.content, () => { computePosition(this.toggle, this.content, { placement: POSITIONS[this.placement] || 'top', strategy: this.strategy || 'fixed', middleware: middleware }).then(({ x, y }: { x: number; y: number }) => { Object.assign(this.content.style, { left: `${x}px`, top: `${y}px` }) }) }) } private _show() { this.content.classList.remove('hidden') if (this.scope === 'window') this.content.classList.add('show') if (this.preventFloatingUI === 'false' && !this.cleanupAutoUpdate) { this.buildFloatingUI() } setTimeout(() => { this.el.classList.add('show') this.fireEvent('show', this.el) dispatch('show.tooltip', this.el, this.el) }) } // Public methods public show() { if (this.eventMode === 'click') { this.click() } else { this.enter() } this.toggle.focus() this.toggle.style.outline = 'none' } public hide() { this.el.classList.remove('show') if (this.scope === 'window') this.content.classList.remove('show') if (this.preventFloatingUI === 'false' && this.cleanupAutoUpdate) { this.cleanupAutoUpdate() this.cleanupAutoUpdate = null } this.fireEvent('hide', this.el) dispatch('hide.tooltip', this.el, this.el) afterTransition(this.content, () => { if (this.el.classList.contains('show')) return false this.content.classList.add('hidden') this.toggle.style.outline = '' }) } public destroy() { // Remove classes this.el.classList.remove('show') this.content.classList.add('hidden') // Remove listeners this.toggle.removeEventListener('focus', this.onToggleFocusListener) this.toggle.removeEventListener('blur', this.onToggleBlurListener) // Remove eventMode-specific listeners if (this.eventMode === 'click') { this.toggle.removeEventListener('click', this.onToggleClickListener) } else if (this.eventMode === 'hover') { this.toggle.removeEventListener('mouseenter', this.onToggleMouseEnterListener) this.toggle.removeEventListener('mouseleave', this.onToggleMouseLeaveListener) } this.toggle.removeEventListener('click', this.onToggleHandleListener, true) this.toggle.removeEventListener('blur', this.onToggleHandleListener, true) if (this.cleanupAutoUpdate) { this.cleanupAutoUpdate() this.cleanupAutoUpdate = null } window.$hsTooltipCollection = window.$hsTooltipCollection.filter(({ element }) => element.el !== this.el) } // Static methods private static findInCollection(target: HSTooltip | HTMLElement | string): ICollectionItem<HSTooltip> | null { return ( window.$hsTooltipCollection.find(el => { if (target instanceof HSTooltip) return el.element.el === target.el else if (typeof target === 'string') { return el.element.el === document.querySelector(target) } else return el.element.el === target }) || null ) } static getInstance(target: HTMLElement | string, isInstance = false) { const elInCollection = window.$hsTooltipCollection.find( el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target) ) return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null } static autoInit() { if (!window.$hsTooltipCollection) window.$hsTooltipCollection = [] if (window.$hsTooltipCollection) { window.$hsTooltipCollection = window.$hsTooltipCollection.filter(({ element }) => document.contains(element.el)) } document.querySelectorAll('.tooltip:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => { if (!window.$hsTooltipCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) { new HSTooltip(el) } }) } static show(target: HSTooltip | HTMLElement | string) { const instance = HSTooltip.findInCollection(target) if (instance) instance.element.show() } static hide(target: HSTooltip | HTMLElement | string) { const instance = HSTooltip.findInCollection(target) if (instance) instance.element.hide() } // Backward compatibility static on(evt: string, target: HSTooltip | HTMLElement | string, cb: Function) { const instance = HSTooltip.findInCollection(target) if (instance) instance.element.events[evt] = cb } } declare global { interface Window { HSTooltip: Function $hsTooltipCollection: ICollectionItem<HSTooltip>[] } } window.addEventListener('load', () => { HSTooltip.autoInit() // Uncomment for debug // console.log('Tooltip collection:', window.$hsTooltipCollection); }) if (typeof window !== 'undefined') { window.HSTooltip = HSTooltip } export default HSTooltip