454 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			454 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const roundByDPR = (value) => {
 | 
						|
    const dpr = window.devicePixelRatio || 1;
 | 
						|
    return Math.round(value * dpr) / dpr;
 | 
						|
};
 | 
						|
 | 
						|
const attributeToOption = (attribute) => {
 | 
						|
    attribute = attribute.replace('tooltip', '');
 | 
						|
    return attribute.charAt(0).toLowerCase() + attribute.slice(1);
 | 
						|
};
 | 
						|
 | 
						|
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
						|
 | 
						|
export const createTooltip = ($el, content, options) => {
 | 
						|
    options = {
 | 
						|
        animation: [
 | 
						|
            [{ opacity: 0 }, { opacity: 1 }],
 | 
						|
            [{ opacity: 1 }, { opacity: 0 }],
 | 
						|
        ],
 | 
						|
        appendTo: document.body,
 | 
						|
        arrow: true,
 | 
						|
        delay: [0, 0],
 | 
						|
        duration: [0, 0],
 | 
						|
        easing: ['linear', 'linear'],
 | 
						|
        hideOnClick: true, // Возможные значения: true, 'all', 'toggle'
 | 
						|
        interactive: true,
 | 
						|
        offset: [0, 8],
 | 
						|
        placement: 'top',
 | 
						|
        shiftPadding: [8, 0],
 | 
						|
        theme: 'light',
 | 
						|
        trigger: 'mouseenter',
 | 
						|
        virtualReference: undefined,
 | 
						|
        zIndex: '',
 | 
						|
        ...options,
 | 
						|
    };
 | 
						|
 | 
						|
    for (const [key, value] of Object.entries($el.dataset)) {
 | 
						|
        if (key.startsWith('tooltip')) {
 | 
						|
            let parsedValue = value;
 | 
						|
            try {
 | 
						|
                parsedValue = JSON.parse(value);
 | 
						|
            } catch {} // eslint-disable-line no-empty
 | 
						|
            options[attributeToOption(key)] = parsedValue;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    let showTimeout;
 | 
						|
    let hideTimeout;
 | 
						|
    let rafId;
 | 
						|
    const listeners = [];
 | 
						|
 | 
						|
    $el._tooltip = {
 | 
						|
        options,
 | 
						|
        isVisible: false,
 | 
						|
        $tooltip: undefined,
 | 
						|
        $container: undefined,
 | 
						|
        $arrow: undefined,
 | 
						|
        $interactive: undefined,
 | 
						|
        autoUpdateCleanup: () => {},
 | 
						|
 | 
						|
        setContent(updatedContent) {
 | 
						|
            if (updatedContent !== undefined) {
 | 
						|
                content = updatedContent;
 | 
						|
            }
 | 
						|
            if ($el._tooltip.$container) {
 | 
						|
                if (content instanceof HTMLElement) {
 | 
						|
                    $el._tooltip.$container.innerHTML = '';
 | 
						|
                    $el._tooltip.$container.append(content);
 | 
						|
                } else {
 | 
						|
                    $el._tooltip.$container.innerHTML = content;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        },
 | 
						|
 | 
						|
        async updateOptions(updatedOptions = {}) {
 | 
						|
            for (const [name, value] of Object.entries(updatedOptions)) {
 | 
						|
                options[name] = value;
 | 
						|
            }
 | 
						|
 | 
						|
            if (updatedOptions.arrow !== undefined && $el._tooltip.$tooltip) {
 | 
						|
                if (options.arrow) {
 | 
						|
                    if (!$el._tooltip.$arrow) {
 | 
						|
                        $el._tooltip.$tooltip.insertAdjacentHTML(
 | 
						|
                            'afterbegin',
 | 
						|
                            `
 | 
						|
                                <div
 | 
						|
                                    class="tooltip__arrow"
 | 
						|
                                    style="pointer-events: none; position: absolute; transform: rotate(45deg);">
 | 
						|
                                </div>
 | 
						|
                            `,
 | 
						|
                        );
 | 
						|
                        $el._tooltip.$arrow = $el._tooltip.$tooltip.querySelector('.tooltip__arrow');
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    $el._tooltip.$arrow?.remove();
 | 
						|
                    $el._tooltip.$arrow = undefined;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (updatedOptions.interactive !== undefined) {
 | 
						|
                for (const { el, event, listener } of listeners) {
 | 
						|
                    el.removeEventListener(event, listener);
 | 
						|
                }
 | 
						|
                listeners.length = 0;
 | 
						|
                registerListeners();
 | 
						|
                if ($el._tooltip.$tooltip) {
 | 
						|
                    if (options.interactive) {
 | 
						|
                        if (!$el._tooltip.$interactive) {
 | 
						|
                            $el._tooltip.$tooltip.insertAdjacentHTML(
 | 
						|
                                'afterbegin',
 | 
						|
                                `
 | 
						|
                                    <div
 | 
						|
                                        class="tooltip__interactive-helper"
 | 
						|
                                        style="position: absolute; z-index: -1;">
 | 
						|
                                    </div>
 | 
						|
                                `,
 | 
						|
                            );
 | 
						|
                            $el._tooltip.$interactive =
 | 
						|
                                $el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper');
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        $el._tooltip.$interactive?.remove();
 | 
						|
                        $el._tooltip.$interactive = undefined;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (updatedOptions.theme !== undefined && $el._tooltip.$tooltip) {
 | 
						|
                const classIndex = [...$el._tooltip.$tooltip.classList].findIndex((className) =>
 | 
						|
                    className.startsWith('tooltip_theme_'),
 | 
						|
                );
 | 
						|
                if (classIndex === -1) {
 | 
						|
                    $el._tooltip.$tooltip.classList.add(`tooltip_theme_${options.theme}`);
 | 
						|
                } else {
 | 
						|
                    $el._tooltip.$tooltip.classList.replace(
 | 
						|
                        $el._tooltip.$tooltip.classList[classIndex],
 | 
						|
                        `tooltip_theme_${options.theme}`,
 | 
						|
                    );
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (updatedOptions.zIndex !== undefined && $el._tooltip.$tooltip) {
 | 
						|
                Object.assign($el._tooltip.$tooltip.style, { zIndex: options.zIndex });
 | 
						|
            }
 | 
						|
 | 
						|
            if ($el._tooltip.$tooltip) {
 | 
						|
                await $el._tooltip.updatePosition();
 | 
						|
            }
 | 
						|
        },
 | 
						|
 | 
						|
        destroy() {
 | 
						|
            if (!$el._tooltip) {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
 | 
						|
            $el._tooltip.$tooltip?.remove();
 | 
						|
 | 
						|
            // Вызываем autoUpdateCleanup только если всплывающая подсказка была видна (иначе вызывать её не имеет смысла)
 | 
						|
            if ($el._tooltip.isVisible) {
 | 
						|
                $el._tooltip.autoUpdateCleanup();
 | 
						|
            }
 | 
						|
 | 
						|
            for (const { el, event, listener } of listeners) {
 | 
						|
                el.removeEventListener(event, listener);
 | 
						|
            }
 | 
						|
            listeners.length = 0;
 | 
						|
 | 
						|
            delete $el._tooltip;
 | 
						|
        },
 | 
						|
    };
 | 
						|
 | 
						|
    if (options.onCreate) {
 | 
						|
        options.onCreate($el._tooltip);
 | 
						|
    }
 | 
						|
 | 
						|
    $el._tooltip.show = async ({ immediately } = {}) => {
 | 
						|
        clearTimeout(hideTimeout);
 | 
						|
 | 
						|
        if (!$el._tooltip.$tooltip) {
 | 
						|
            const { computePosition, offset, flip, shift, arrow } = await import('@floating-ui/dom');
 | 
						|
 | 
						|
            $el._tooltip.$tooltip = document.createElement('div');
 | 
						|
            $el._tooltip.$tooltip._reference = $el;
 | 
						|
            $el._tooltip.$tooltip.classList.add('tooltip', `tooltip_theme_${options.theme}`);
 | 
						|
            Object.assign($el._tooltip.$tooltip.style, { zIndex: options.zIndex });
 | 
						|
 | 
						|
            $el._tooltip.$tooltip.innerHTML = `
 | 
						|
                <div class="tooltip__root">
 | 
						|
                    ${
 | 
						|
                        options.interactive
 | 
						|
                            ? `
 | 
						|
                                <div
 | 
						|
                                    class="tooltip__interactive-helper"
 | 
						|
                                    style="position: absolute; z-index: -1;">
 | 
						|
                                </div>
 | 
						|
                            `
 | 
						|
                            : ''
 | 
						|
                    }
 | 
						|
                    ${
 | 
						|
                        options.arrow
 | 
						|
                            ? `
 | 
						|
                                <div
 | 
						|
                                    class="tooltip__arrow"
 | 
						|
                                    style="pointer-events: none; position: absolute; transform: rotate(45deg);">
 | 
						|
                                </div>
 | 
						|
                            `
 | 
						|
                            : ''
 | 
						|
                    }
 | 
						|
                    <div class="tooltip__container"></div>
 | 
						|
                </div>
 | 
						|
            `;
 | 
						|
 | 
						|
            $el._tooltip.$container = $el._tooltip.$tooltip.querySelector('.tooltip__container');
 | 
						|
            $el._tooltip.$arrow = $el._tooltip.$tooltip.querySelector('.tooltip__arrow');
 | 
						|
            $el._tooltip.$interactive = $el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper');
 | 
						|
 | 
						|
            $el._tooltip.setContent();
 | 
						|
 | 
						|
            $el._tooltip.updatePosition = () => {
 | 
						|
                cancelAnimationFrame(rafId);
 | 
						|
                rafId = requestAnimationFrame(async () => {
 | 
						|
                    const { x, y, placement, middlewareData } = await computePosition(
 | 
						|
                        options.virtualReference ?? $el,
 | 
						|
                        $el._tooltip.$tooltip,
 | 
						|
                        {
 | 
						|
                            placement: options.placement,
 | 
						|
                            middleware: [
 | 
						|
                                offset({ mainAxis: options.offset[1], crossAxis: options.offset[0] }),
 | 
						|
                                flip(),
 | 
						|
                                shift({
 | 
						|
                                    padding: {
 | 
						|
                                        top: options.shiftPadding[1],
 | 
						|
                                        right: options.shiftPadding[0],
 | 
						|
                                        bottom: options.shiftPadding[1],
 | 
						|
                                        left: options.shiftPadding[0],
 | 
						|
                                    },
 | 
						|
                                }),
 | 
						|
                                arrow({ element: $el._tooltip.$arrow }),
 | 
						|
                            ],
 | 
						|
                        },
 | 
						|
                    );
 | 
						|
 | 
						|
                    Object.assign($el._tooltip.$tooltip.style, {
 | 
						|
                        transform: `translate(${roundByDPR(x)}px, ${roundByDPR(y)}px)`,
 | 
						|
                    });
 | 
						|
 | 
						|
                    const side = placement.split('-')[0];
 | 
						|
                    const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[side];
 | 
						|
 | 
						|
                    $el._tooltip.$tooltip.classList.remove(
 | 
						|
                        'tooltip_side_top',
 | 
						|
                        'tooltip_side_bottom',
 | 
						|
                        'tooltip_side_left',
 | 
						|
                        'tooltip_side_right',
 | 
						|
                    );
 | 
						|
                    $el._tooltip.$tooltip.classList.add(`tooltip_side_${side}`);
 | 
						|
 | 
						|
                    if ($el._tooltip.$interactive) {
 | 
						|
                        Object.assign($el._tooltip.$interactive.style, {
 | 
						|
                            width: side === 'left' || side === 'right' ? `${options.offset[1]}px` : '100%',
 | 
						|
                            height: side === 'top' || side === 'bottom' ? `${options.offset[1]}px` : '100%',
 | 
						|
                            [staticSide]: `-${options.offset[1]}px`,
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
 | 
						|
                    if ($el._tooltip.$arrow && middlewareData.arrow) {
 | 
						|
                        const { x, y } = middlewareData.arrow;
 | 
						|
                        Object.assign($el._tooltip.$arrow.style, {
 | 
						|
                            left: x === undefined ? '' : `${x}px`,
 | 
						|
                            top: y === undefined ? '' : `${y}px`,
 | 
						|
                            [staticSide]: `-${$el._tooltip.$arrow.offsetWidth / 2}px`,
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            };
 | 
						|
 | 
						|
            if (options.onMount) {
 | 
						|
                options.onMount($el._tooltip);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        const { autoUpdate } = await import('@floating-ui/dom');
 | 
						|
 | 
						|
        showTimeout = setTimeout(
 | 
						|
            async () => {
 | 
						|
                if (!$el._tooltip.isVisible) {
 | 
						|
                    (options.appendTo === 'parent' ? $el.parentElement : options.appendTo).append(
 | 
						|
                        $el._tooltip.$tooltip,
 | 
						|
                    );
 | 
						|
                    $el._tooltip.isVisible = true;
 | 
						|
                    $el._tooltip.autoUpdateCleanup = autoUpdate(
 | 
						|
                        $el,
 | 
						|
                        $el._tooltip.$tooltip,
 | 
						|
                        $el._tooltip.updatePosition,
 | 
						|
                    );
 | 
						|
 | 
						|
                    if (
 | 
						|
                        options.hideOnClick &&
 | 
						|
                        (options.trigger.includes('click') || options.trigger.includes('manual'))
 | 
						|
                    ) {
 | 
						|
                        document.body.addEventListener('click', $el._tooltip.hideOnClickListener);
 | 
						|
                        listeners.push({
 | 
						|
                            el: document.body,
 | 
						|
                            event: 'click',
 | 
						|
                            listener: $el._tooltip.hideOnClickListener,
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (options.onShow) {
 | 
						|
                        options.onShow($el._tooltip);
 | 
						|
                    }
 | 
						|
 | 
						|
                    try {
 | 
						|
                        await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[0], {
 | 
						|
                            duration: immediately ? 0 : options.duration[0],
 | 
						|
                            easing: options.easing[0],
 | 
						|
                        }).finished;
 | 
						|
                    } catch {} // eslint-disable-line no-empty
 | 
						|
 | 
						|
                    if (options.onShown) {
 | 
						|
                        options.onShown($el._tooltip);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
            immediately ? 0 : options.delay[0],
 | 
						|
        );
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.hide = ({ immediately } = {}) => {
 | 
						|
        clearTimeout(showTimeout);
 | 
						|
        hideTimeout = setTimeout(
 | 
						|
            async () => {
 | 
						|
                if ($el._tooltip.isVisible) {
 | 
						|
                    if (options.onHide) {
 | 
						|
                        options.onHide($el._tooltip);
 | 
						|
                    }
 | 
						|
 | 
						|
                    try {
 | 
						|
                        await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[1], {
 | 
						|
                            duration: immediately ? 0 : options.duration[1],
 | 
						|
                            easing: options.easing[1],
 | 
						|
                        }).finished;
 | 
						|
                    } catch {} // eslint-disable-line no-empty
 | 
						|
 | 
						|
                    if ($el._tooltip.$tooltip) {
 | 
						|
                        $el._tooltip.$tooltip.remove();
 | 
						|
                    }
 | 
						|
                    $el._tooltip.isVisible = false;
 | 
						|
                    $el._tooltip.autoUpdateCleanup();
 | 
						|
 | 
						|
                    if (options.onHidden) {
 | 
						|
                        options.onHidden($el._tooltip);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
            immediately ? 0 : options.delay[1],
 | 
						|
        );
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.hideOnClickListener = ({ target }) => {
 | 
						|
        if (!$el._tooltip.isVisible) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (options.hideOnClick === 'all') {
 | 
						|
            $el._tooltip.hide();
 | 
						|
            document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
 | 
						|
        } else if (options.hideOnClick === 'toggle') {
 | 
						|
            if ($el.contains(target)) {
 | 
						|
                $el._tooltip.hide();
 | 
						|
                document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            // options.hideOnClick === true
 | 
						|
            if ($el.contains(target) || !$el._tooltip.$tooltip.contains(target)) {
 | 
						|
                $el._tooltip.hide();
 | 
						|
                document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.clickListener = () => {
 | 
						|
        if (!$el._tooltip.isVisible) {
 | 
						|
            $el._tooltip.show();
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.mouseEnterListener = () => {
 | 
						|
        $el._tooltip.show();
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.mouseLeaveListener = ({ relatedTarget }) => {
 | 
						|
        if (options.interactive && relatedTarget && $el._tooltip.$tooltip?.contains(relatedTarget)) {
 | 
						|
            $el._tooltip.$tooltip.addEventListener(
 | 
						|
                'mouseleave',
 | 
						|
                ({ relatedTarget }) => {
 | 
						|
                    if (!relatedTarget || !$el.contains(relatedTarget)) {
 | 
						|
                        $el._tooltip.hide();
 | 
						|
                    }
 | 
						|
                },
 | 
						|
                { once: true },
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            $el._tooltip.hide();
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.focusListener = () => {
 | 
						|
        $el._tooltip.show();
 | 
						|
    };
 | 
						|
 | 
						|
    $el._tooltip.blurListener = () => {
 | 
						|
        $el._tooltip.hide();
 | 
						|
    };
 | 
						|
 | 
						|
    const registerListeners = () => {
 | 
						|
        for (const trigger of options.trigger.split(' ')) {
 | 
						|
            switch (trigger) {
 | 
						|
                case 'mouseenter': {
 | 
						|
                    if (!isTouchDevice()) {
 | 
						|
                        $el.addEventListener('mouseenter', $el._tooltip.mouseEnterListener);
 | 
						|
                        listeners.push({ el: $el, event: 'mouseenter', listener: $el._tooltip.mouseEnterListener });
 | 
						|
 | 
						|
                        $el.addEventListener('mouseleave', $el._tooltip.mouseLeaveListener);
 | 
						|
                        listeners.push({ el: $el, event: 'mouseleave', listener: $el._tooltip.mouseLeaveListener });
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
                case 'click': {
 | 
						|
                    $el.addEventListener('click', $el._tooltip.clickListener);
 | 
						|
                    listeners.push({ el: $el, event: 'click', listener: $el._tooltip.clickListener });
 | 
						|
 | 
						|
                    if (!options.interactive) {
 | 
						|
                        $el.addEventListener('mouseleave', $el._tooltip.mouseLeaveListener);
 | 
						|
                        listeners.push({ el: $el, event: 'mouseleave', listener: $el._tooltip.mouseLeaveListener });
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
                case 'focus': {
 | 
						|
                    $el.addEventListener('focus', $el._tooltip.focusListener);
 | 
						|
                    listeners.push({ el: $el, event: 'focus', listener: $el._tooltip.focusListener });
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
                case 'blur': {
 | 
						|
                    $el.addEventListener('blur', $el._tooltip.blurListener);
 | 
						|
                    listeners.push({ el: $el, event: 'blur', listener: $el._tooltip.blurListener });
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    registerListeners();
 | 
						|
};
 |