7 Commits

3 changed files with 143 additions and 82 deletions

View File

@@ -1,26 +1,39 @@
## v6.0.0
- Корректная работа сообытий mouseenter/mouseleave на тач-устройствах
- Предовтращаем лишние показы всплывающей подсказки
## v5.0.0
- Регистрируем сообытия mouseenter/mouseleave на тач-устройствах
## v4.1.1
- Исправление зависаний (в некоторых случаях) всплывающих подсказок
## v4.1.0 ## v4.1.0
- Отключаем события mouseenter/mouseleave для toch-устройств - Отключаем события mouseenter/mouseleave для toch-устройств
## v4.0.0 ## v4.0.0
- Переработана логика инициализации и обновления тултипа - Переработана логика инициализации и обновления тултипа
- Добавлены новые триггеры: `focus`, `blur` - Добавлены новые триггеры: `focus`, `blur`
- Опция `hideOnClick` теперь поддерживает значения: `true`, `'all'`, `'toggle'` - Опция `hideOnClick` теперь поддерживает значения: `true`, `'all'`, `'toggle'`
- Исправлены ошибки с позиционированием, анимацией и стилями - Исправлены ошибки с позиционированием, анимацией и стилями
- Оптимизировано управление событиями и очистка ресурсов - Оптимизировано управление событиями и очистка ресурсов
## v3.0.1 ## v3.0.1
- Исправлена работа событий - Исправлена работа событий
## v3.0.0 ## v3.0.0
- Исправлено название опции с `virtialReference` на `virtualReference` - Исправлено название опции с `virtialReference` на `virtualReference`
## v2.1.0 ## v2.1.0
- Для опции `appendTo` добавлена возможность указать значение `parent` - Для опции `appendTo` добавлена возможность указать значение `parent`
## v2.0.0 ## v2.0.0

192
index.js
View File

@@ -8,7 +8,22 @@ const attributeToOption = (attribute) => {
return attribute.charAt(0).toLowerCase() + attribute.slice(1); return attribute.charAt(0).toLowerCase() + attribute.slice(1);
}; };
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0; let visibilityListenerRegistered = false;
const handleVisibilityChange = () => {
if (document.hidden) {
// Скрываем все активные всплывающие подсказки
for (const $tooltip of document.querySelectorAll('.tooltip')) {
const ref = $tooltip._reference;
if (ref?._tooltip) {
// Очищаем таймеры появления при скрытии страницы
clearTimeout(ref._tooltip._showTimeout);
if (ref._tooltip.isVisible) {
ref._tooltip.hide({ immediately: true });
}
}
}
}
};
export const createTooltip = ($el, content, options) => { export const createTooltip = ($el, content, options) => {
options = { options = {
@@ -43,8 +58,6 @@ export const createTooltip = ($el, content, options) => {
} }
} }
let showTimeout;
let hideTimeout;
let rafId; let rafId;
const listeners = []; const listeners = [];
@@ -55,6 +68,8 @@ export const createTooltip = ($el, content, options) => {
$container: undefined, $container: undefined,
$arrow: undefined, $arrow: undefined,
$interactive: undefined, $interactive: undefined,
_showTimeout: undefined,
_hideTimeout: undefined,
autoUpdateCleanup: () => {}, autoUpdateCleanup: () => {},
setContent(updatedContent) { setContent(updatedContent) {
@@ -152,6 +167,10 @@ export const createTooltip = ($el, content, options) => {
return; return;
} }
clearTimeout($el._tooltip._showTimeout);
clearTimeout($el._tooltip._hideTimeout);
cancelAnimationFrame(rafId);
$el._tooltip.$tooltip?.remove(); $el._tooltip.$tooltip?.remove();
// Вызываем autoUpdateCleanup только если всплывающая подсказка была видна (иначе вызывать её не имеет смысла) // Вызываем autoUpdateCleanup только если всплывающая подсказка была видна (иначе вызывать её не имеет смысла)
@@ -173,7 +192,7 @@ export const createTooltip = ($el, content, options) => {
} }
$el._tooltip.show = async ({ immediately } = {}) => { $el._tooltip.show = async ({ immediately } = {}) => {
clearTimeout(hideTimeout); clearTimeout($el._tooltip._hideTimeout);
if (!$el._tooltip.$tooltip) { if (!$el._tooltip.$tooltip) {
const { computePosition, offset, flip, shift, arrow } = await import('@floating-ui/dom'); const { computePosition, offset, flip, shift, arrow } = await import('@floating-ui/dom');
@@ -280,45 +299,47 @@ export const createTooltip = ($el, content, options) => {
const { autoUpdate } = await import('@floating-ui/dom'); const { autoUpdate } = await import('@floating-ui/dom');
showTimeout = setTimeout( $el._tooltip._showTimeout = setTimeout(
async () => { async () => {
if (!$el._tooltip.isVisible) { const isMouseEnterTrigger = options.trigger.includes('mouseenter');
(options.appendTo === 'parent' ? $el.parentElement : options.appendTo).append( const isHovering = $el.matches(':hover');
$el._tooltip.$tooltip,
);
$el._tooltip.isVisible = true;
$el._tooltip.autoUpdateCleanup = autoUpdate(
$el,
$el._tooltip.$tooltip,
$el._tooltip.updatePosition,
);
if ( // Проверяем $el._tooltip на сущестование и актуальность показа всплывающей подсказки
options.hideOnClick && if (
(options.trigger.includes('click') || options.trigger.includes('manual')) !$el._tooltip ||
) { $el._tooltip.isVisible ||
document.body.addEventListener('click', $el._tooltip.hideOnClickListener); (isMouseEnterTrigger && !isHovering) ||
listeners.push({ document.hidden
el: document.body, ) {
event: 'click', return;
listener: $el._tooltip.hideOnClickListener, }
});
}
if (options.onShow) { (options.appendTo === 'parent' ? $el.parentElement : options.appendTo).append($el._tooltip.$tooltip);
options.onShow($el._tooltip); $el._tooltip.isVisible = true;
} $el._tooltip.autoUpdateCleanup = autoUpdate($el, $el._tooltip.$tooltip, $el._tooltip.updatePosition);
try { if (options.hideOnClick && (options.trigger.includes('click') || options.trigger.includes('manual'))) {
await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[0], { document.body.addEventListener('click', $el._tooltip.hideOnClickListener);
duration: immediately ? 0 : options.duration[0], listeners.push({
easing: options.easing[0], el: document.body,
}).finished; event: 'click',
} catch {} // eslint-disable-line no-empty listener: $el._tooltip.hideOnClickListener,
});
}
if (options.onShown) { if (options.onShow) {
options.onShown($el._tooltip); 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], immediately ? 0 : options.delay[0],
@@ -326,30 +347,38 @@ export const createTooltip = ($el, content, options) => {
}; };
$el._tooltip.hide = ({ immediately } = {}) => { $el._tooltip.hide = ({ immediately } = {}) => {
clearTimeout(showTimeout); clearTimeout($el._tooltip._showTimeout);
hideTimeout = setTimeout( $el._tooltip._hideTimeout = setTimeout(
async () => { async () => {
if ($el._tooltip.isVisible) { // Проверяем $el._tooltip на сущестование
if (options.onHide) { if (!$el._tooltip || !$el._tooltip.isVisible) {
options.onHide($el._tooltip); return;
} }
try { if (options.onHide) {
await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[1], { options.onHide($el._tooltip);
duration: immediately ? 0 : options.duration[1], }
easing: options.easing[1],
}).finished;
} catch {} // eslint-disable-line no-empty
if ($el._tooltip.$tooltip) { try {
$el._tooltip.$tooltip.remove(); await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[1], {
} duration: immediately ? 0 : options.duration[1],
$el._tooltip.isVisible = false; easing: options.easing[1],
$el._tooltip.autoUpdateCleanup(); }).finished;
} catch {} // eslint-disable-line no-empty
if (options.onHidden) { // Ещё одна проверка на сущестование $el._tooltip после await
options.onHidden($el._tooltip); if (!$el._tooltip) {
} return;
}
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], immediately ? 0 : options.delay[1],
@@ -379,21 +408,34 @@ export const createTooltip = ($el, content, options) => {
}; };
$el._tooltip.clickListener = () => { $el._tooltip.clickListener = () => {
if (document.hidden) {
return;
}
if (!$el._tooltip.isVisible) { if (!$el._tooltip.isVisible) {
$el._tooltip.show(); $el._tooltip.show();
} }
}; };
$el._tooltip.mouseEnterListener = () => { $el._tooltip.mouseEnterListener = (event) => {
if (event.pointerType !== 'mouse' || document.hidden) {
return;
}
$el._tooltip.show(); $el._tooltip.show();
}; };
$el._tooltip.mouseLeaveListener = ({ relatedTarget }) => { $el._tooltip.mouseLeaveListener = (event) => {
if (options.interactive && relatedTarget && $el._tooltip.$tooltip?.contains(relatedTarget)) { if (event.pointerType !== 'mouse') {
return;
}
if (options.interactive && event.relatedTarget && $el._tooltip.$tooltip?.contains(event.relatedTarget)) {
$el._tooltip.$tooltip.addEventListener( $el._tooltip.$tooltip.addEventListener(
'mouseleave', 'pointerleave',
({ relatedTarget }) => { (event) => {
if (!relatedTarget || !$el.contains(relatedTarget)) { if (event.pointerType !== 'mouse') {
return;
}
if (!event.relatedTarget || !$el.contains(event.relatedTarget)) {
$el._tooltip.hide(); $el._tooltip.hide();
} }
}, },
@@ -405,6 +447,9 @@ export const createTooltip = ($el, content, options) => {
}; };
$el._tooltip.focusListener = () => { $el._tooltip.focusListener = () => {
if (document.hidden) {
return;
}
$el._tooltip.show(); $el._tooltip.show();
}; };
@@ -416,13 +461,11 @@ export const createTooltip = ($el, content, options) => {
for (const trigger of options.trigger.split(' ')) { for (const trigger of options.trigger.split(' ')) {
switch (trigger) { switch (trigger) {
case 'mouseenter': { case 'mouseenter': {
if (!isTouchDevice()) { $el.addEventListener('pointerenter', $el._tooltip.mouseEnterListener);
$el.addEventListener('mouseenter', $el._tooltip.mouseEnterListener); listeners.push({ el: $el, event: 'pointerenter', listener: $el._tooltip.mouseEnterListener });
listeners.push({ el: $el, event: 'mouseenter', listener: $el._tooltip.mouseEnterListener });
$el.addEventListener('mouseleave', $el._tooltip.mouseLeaveListener); $el.addEventListener('pointerleave', $el._tooltip.mouseLeaveListener);
listeners.push({ el: $el, event: 'mouseleave', listener: $el._tooltip.mouseLeaveListener }); listeners.push({ el: $el, event: 'pointerleave', listener: $el._tooltip.mouseLeaveListener });
}
break; break;
} }
case 'click': { case 'click': {
@@ -430,8 +473,8 @@ export const createTooltip = ($el, content, options) => {
listeners.push({ el: $el, event: 'click', listener: $el._tooltip.clickListener }); listeners.push({ el: $el, event: 'click', listener: $el._tooltip.clickListener });
if (!options.interactive) { if (!options.interactive) {
$el.addEventListener('mouseleave', $el._tooltip.mouseLeaveListener); $el.addEventListener('pointerleave', $el._tooltip.mouseLeaveListener);
listeners.push({ el: $el, event: 'mouseleave', listener: $el._tooltip.mouseLeaveListener }); listeners.push({ el: $el, event: 'pointerleave', listener: $el._tooltip.mouseLeaveListener });
} }
break; break;
} }
@@ -450,4 +493,9 @@ export const createTooltip = ($el, content, options) => {
}; };
registerListeners(); registerListeners();
if (!visibilityListenerRegistered) {
document.addEventListener('visibilitychange', handleVisibilityChange);
visibilityListenerRegistered = true;
}
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "@advdominion/tooltip", "name": "@advdominion/tooltip",
"version": "4.1.0", "version": "6.0.0",
"type": "module", "type": "module",
"packageManager": "yarn@4.9.4", "packageManager": "yarn@4.9.4",
"main": "index.js", "main": "index.js",