11 Commits

7 changed files with 1054 additions and 1032 deletions

940
.yarn/releases/yarn-4.14.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,3 @@
compressionLevel: mixed
enableGlobalCache: false enableGlobalCache: false
yarnPath: .yarn/releases/yarn-4.9.4.cjs yarnPath: .yarn/releases/yarn-4.14.1.cjs

View File

@@ -1,3 +1,21 @@
## v7.0.1
- Исправление работы анимации
## v7.0.0
- Изменено значение по умолчанию для параметра `trigger`
- Рефакторинг кода и устранение ошибок
## v6.0.0
- Корректная работа сообытий mouseenter/mouseleave на тач-устройствах
- Предовтращаем лишние показы всплывающей подсказки
## v5.0.0
- Регистрируем сообытия mouseenter/mouseleave на тач-устройствах
## v4.1.1 ## v4.1.1
- Исправление зависаний (в некоторых случаях) всплывающих подсказок - Исправление зависаний (в некоторых случаях) всплывающих подсказок

View File

@@ -43,7 +43,7 @@ createTooltip(document.querySelector('button'), 'Подсказка', {
placement: 'top', placement: 'top',
shiftPadding: [8, 0], shiftPadding: [8, 0],
theme: 'light', theme: 'light',
trigger: 'mouseenter', trigger: 'mouseenter click',
virtualReference: undefined, virtualReference: undefined,
zIndex: '', zIndex: '',
// Callback-функции, по умолчанию не заданы // Callback-функции, по умолчанию не заданы

156
index.js
View File

@@ -8,18 +8,30 @@ 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; let visibilityListenerRegistered = false;
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.hidden) { if (document.hidden) {
// Скрываем все активные всплывающие подсказки // Скрываем все активные всплывающие подсказки
for (const $tooltip of document.querySelectorAll('.tooltip')) { for (const $tooltip of document.querySelectorAll('.tooltip')) {
if ($tooltip._reference?._tooltip?.isVisible) { const ref = $tooltip._reference;
$tooltip._reference._tooltip.hide({ immediately: true }); if (ref?._tooltip) {
// Очищаем таймеры появления при скрытии страницы
clearTimeout(ref._tooltip._showTimeout);
if (ref._tooltip.isVisible) {
ref._tooltip.hide({ immediately: true });
} }
} }
} }
}
};
const templates = {
arrow: () => `
<div class="tooltip__arrow" style="pointer-events: none; position: absolute; transform: rotate(45deg);"></div>
`,
interactiveHelper: () => `
<div class="tooltip__interactive-helper" style="position: absolute; z-index: -1;"></div>
`,
}; };
export const createTooltip = ($el, content, options) => { export const createTooltip = ($el, content, options) => {
@@ -39,7 +51,7 @@ export const createTooltip = ($el, content, options) => {
placement: 'top', placement: 'top',
shiftPadding: [8, 0], shiftPadding: [8, 0],
theme: 'light', theme: 'light',
trigger: 'mouseenter', trigger: 'mouseenter click',
virtualReference: undefined, virtualReference: undefined,
zIndex: '', zIndex: '',
...options, ...options,
@@ -55,8 +67,6 @@ export const createTooltip = ($el, content, options) => {
} }
} }
let showTimeout;
let hideTimeout;
let rafId; let rafId;
const listeners = []; const listeners = [];
@@ -67,7 +77,11 @@ export const createTooltip = ($el, content, options) => {
$container: undefined, $container: undefined,
$arrow: undefined, $arrow: undefined,
$interactive: undefined, $interactive: undefined,
_showTimeout: undefined,
_hideTimeout: undefined,
_currentAnimation: undefined,
autoUpdateCleanup: () => {}, autoUpdateCleanup: () => {},
updatePosition: async () => {},
setContent(updatedContent) { setContent(updatedContent) {
if (updatedContent !== undefined) { if (updatedContent !== undefined) {
@@ -91,15 +105,7 @@ export const createTooltip = ($el, content, options) => {
if (updatedOptions.arrow !== undefined && $el._tooltip.$tooltip) { if (updatedOptions.arrow !== undefined && $el._tooltip.$tooltip) {
if (options.arrow) { if (options.arrow) {
if (!$el._tooltip.$arrow) { if (!$el._tooltip.$arrow) {
$el._tooltip.$tooltip.insertAdjacentHTML( $el._tooltip.$tooltip.querySelector('.tooltip__root').insertAdjacentHTML('afterbegin', templates.arrow());
'afterbegin',
`
<div
class="tooltip__arrow"
style="pointer-events: none; position: absolute; transform: rotate(45deg);">
</div>
`,
);
$el._tooltip.$arrow = $el._tooltip.$tooltip.querySelector('.tooltip__arrow'); $el._tooltip.$arrow = $el._tooltip.$tooltip.querySelector('.tooltip__arrow');
} }
} else { } else {
@@ -117,15 +123,7 @@ export const createTooltip = ($el, content, options) => {
if ($el._tooltip.$tooltip) { if ($el._tooltip.$tooltip) {
if (options.interactive) { if (options.interactive) {
if (!$el._tooltip.$interactive) { if (!$el._tooltip.$interactive) {
$el._tooltip.$tooltip.insertAdjacentHTML( $el._tooltip.$tooltip.querySelector('.tooltip__root').insertAdjacentHTML('afterbegin', templates.interactiveHelper());
'afterbegin',
`
<div
class="tooltip__interactive-helper"
style="position: absolute; z-index: -1;">
</div>
`,
);
$el._tooltip.$interactive = $el._tooltip.$interactive =
$el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper'); $el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper');
} }
@@ -164,8 +162,9 @@ export const createTooltip = ($el, content, options) => {
return; return;
} }
clearTimeout(showTimeout); clearTimeout($el._tooltip._showTimeout);
clearTimeout(hideTimeout); clearTimeout($el._tooltip._hideTimeout);
$el._tooltip._currentAnimation?.cancel();
cancelAnimationFrame(rafId); cancelAnimationFrame(rafId);
$el._tooltip.$tooltip?.remove(); $el._tooltip.$tooltip?.remove();
@@ -180,6 +179,8 @@ export const createTooltip = ($el, content, options) => {
} }
listeners.length = 0; listeners.length = 0;
document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
delete $el._tooltip; delete $el._tooltip;
}, },
}; };
@@ -189,7 +190,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');
@@ -201,26 +202,8 @@ export const createTooltip = ($el, content, options) => {
$el._tooltip.$tooltip.innerHTML = ` $el._tooltip.$tooltip.innerHTML = `
<div class="tooltip__root"> <div class="tooltip__root">
${ ${options.interactive ? templates.interactiveHelper() : ''}
options.interactive ${options.arrow ? templates.arrow() : ''}
? `
<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 class="tooltip__container"></div>
</div> </div>
`; `;
@@ -296,10 +279,18 @@ 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 () => {
// Проверяем $el._tooltip на сущестование const isMouseEnterTrigger = options.trigger.includes('mouseenter');
if (!$el._tooltip || $el._tooltip.isVisible) { const isHovering = $el.matches(':hover');
// Проверяем $el._tooltip на сущестование и актуальность показа всплывающей подсказки
if (
!$el._tooltip ||
$el._tooltip.isVisible ||
(isMouseEnterTrigger && !isHovering) ||
document.hidden
) {
return; return;
} }
@@ -321,10 +312,12 @@ export const createTooltip = ($el, content, options) => {
} }
try { try {
await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[0], { const animation = $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[0], {
duration: immediately ? 0 : options.duration[0], duration: immediately ? 0 : options.duration[0],
easing: options.easing[0], easing: options.easing[0],
}).finished; });
$el._tooltip._currentAnimation = animation;
await animation.finished;
} catch {} // eslint-disable-line no-empty } catch {} // eslint-disable-line no-empty
if (options.onShown) { if (options.onShown) {
@@ -336,8 +329,8 @@ 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 () => {
// Проверяем $el._tooltip на сущестование // Проверяем $el._tooltip на сущестование
if (!$el._tooltip || !$el._tooltip.isVisible) { if (!$el._tooltip || !$el._tooltip.isVisible) {
@@ -348,11 +341,15 @@ export const createTooltip = ($el, content, options) => {
options.onHide($el._tooltip); options.onHide($el._tooltip);
} }
document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
try { try {
await $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[1], { const animation = $el._tooltip.$tooltip.querySelector('.tooltip__root').animate(options.animation[1], {
duration: immediately ? 0 : options.duration[1], duration: immediately ? 0 : options.duration[1],
easing: options.easing[1], easing: options.easing[1],
}).finished; });
$el._tooltip._currentAnimation = animation;
await animation.finished;
} catch {} // eslint-disable-line no-empty } catch {} // eslint-disable-line no-empty
// Ещё одна проверка на сущестование $el._tooltip после await // Ещё одна проверка на сущестование $el._tooltip после await
@@ -381,37 +378,47 @@ export const createTooltip = ($el, content, options) => {
if (options.hideOnClick === 'all') { if (options.hideOnClick === 'all') {
$el._tooltip.hide(); $el._tooltip.hide();
document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
} else if (options.hideOnClick === 'toggle') { } else if (options.hideOnClick === 'toggle') {
if ($el.contains(target)) { if ($el.contains(target)) {
$el._tooltip.hide(); $el._tooltip.hide();
document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
} }
} else { } else {
// options.hideOnClick === true // options.hideOnClick === true
if ($el.contains(target) || !$el._tooltip.$tooltip.contains(target)) { if ($el.contains(target) || !$el._tooltip.$tooltip.contains(target)) {
$el._tooltip.hide(); $el._tooltip.hide();
document.body.removeEventListener('click', $el._tooltip.hideOnClickListener);
} }
} }
}; };
$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();
} }
}, },
@@ -423,6 +430,9 @@ export const createTooltip = ($el, content, options) => {
}; };
$el._tooltip.focusListener = () => { $el._tooltip.focusListener = () => {
if (document.hidden) {
return;
}
$el._tooltip.show(); $el._tooltip.show();
}; };
@@ -434,13 +444,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': {
@@ -448,8 +456,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;
} }

View File

@@ -1,8 +1,8 @@
{ {
"name": "@advdominion/tooltip", "name": "@advdominion/tooltip",
"version": "4.1.1", "version": "7.0.1",
"type": "module", "type": "module",
"packageManager": "yarn@4.9.4", "packageManager": "yarn@4.14.1",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
"type": "git", "type": "git",