zero-tooltip 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tooltip.ts CHANGED
@@ -1,573 +1,678 @@
1
- import { Directive, isReactive, watch } from "vue"
2
- import { v4 as uuidv4 } from 'uuid'
3
- import TooltipConfig from "./types/tooltipConfig"
4
- import TooltipPosition from "./types/tooltipPosition"
5
- import TooltipPositions from "./types/tooltipPositions"
6
- import TooltipLocalConfig from "./types/tooltipLocalConfig"
7
- import useHideOnScroll from './composables/useHideOnScroll'
8
- import useHideOnResize from "./composables/useHideOnResize"
9
-
10
- const { handleHideOnScroll } = useHideOnScroll()
11
- const { handleHideOnResize, resetResizeReferences } = useHideOnResize()
12
-
13
- const tooltipElementClass = 'zero-tooltip__container'
14
- const textElementClass = 'zero-tooltip__text'
15
- const arrowElementClass = 'zero-tooltip__arrow'
16
-
17
- // For each TooltipPosition define sequence of positions that will be checked when determining where to render Tooltip
18
- // Meant as fallback positions in case Tooltip do not have enough space in originally set position
19
- const defaultTooltipPositions: TooltipPositions = {
20
- left: ['left', 'right', 'top', 'bottom'],
21
- top: ['top', 'bottom', 'right', 'left'],
22
- right: ['right', 'left', 'top', 'bottom'],
23
- bottom: ['bottom', 'top', 'right', 'left'],
24
- }
25
-
26
- const defaultAppendTo: string = 'body'
27
- const defaultTooltipPosition: TooltipPosition = 'top'
28
- const defaultTooltipOffsetFromSource = 10
29
- const defaultTooltipOffsetFromViewport = 20
30
- const defaultTooltipMinWidth = 100
31
- const defaultTooltipMaxWidth = 250
32
- const defaultTooltipBorderWidth = 0
33
- const defaultTooltipClasses = 'zt-fixed zt-opacity-0 zt-inline-block zt-w-fit zt-py-1.5 zt-px-2.5 zt-rounded-md zt-bg-[#495057] zt-shadow-[0_2px_12px_0_rgba(0,0,0,0.1)] zt-box-border'
34
- const defaultTextClasses = 'zt-text-sm zt-text-white zt-whitespace-pre-wrap zt-break-words'
35
- const defaultArrowSize = 5
36
- const defaultArrowClasses = 'zt-absolute zt-border-solid zt-border-[#495057]'
37
- const defaultMinArrowOffsetFromTooltipCorner = 6
38
- const defaultZIndex = 1
39
- const defaultShouldShow = true
40
- const defaultShowDelay = 0
41
- const defaultHideDelay = 0
42
-
43
- const tooltips: {[key: string]: ReturnType<typeof initTooltip>} = {}
44
-
45
- const ZeroTooltip = (globalConfig?: TooltipConfig): Directive => {
46
- return {
47
- created: (targetElement: HTMLElement, binding, vnode) => {
48
- const uuid = uuidv4()
49
- vnode.el.$_tooltip = { uuid: uuid }
50
-
51
- buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
52
-
53
- if (typeof(binding.value) !== 'string' && isReactive(binding.value)) {
54
- watch(binding.value, (newBindingValue) => {
55
- if (tooltips[uuid]) {
56
- destroyTooltip(tooltips[uuid])
57
- }
58
-
59
- buildTooltip(newBindingValue, globalConfig, binding.arg, targetElement, uuid)
60
- })
61
- }
62
- },
63
-
64
- updated: (targetElement: HTMLElement, binding, vnode) => {
65
- const uuid = vnode.el.$_tooltip.uuid
66
-
67
- if (tooltips[uuid]) {
68
- destroyTooltip(tooltips[uuid])
69
- }
70
-
71
- buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
72
- },
73
-
74
- beforeUnmount: (_, __, vnode) => {
75
- const uuid = vnode.el.$_tooltip.uuid
76
-
77
- if (tooltips[uuid]) {
78
- destroyTooltip(tooltips[uuid])
79
- }
80
- }
81
- }
82
- }
83
-
84
- function buildTooltip(bindingValue: any, globalConfig: TooltipConfig | undefined, bindingArgument: string | undefined, targetElement: HTMLElement, uuid: string) {
85
- let tooltipConfig = getTooltipConfig(bindingValue as string | TooltipLocalConfig, globalConfig, bindingArgument as TooltipPosition)
86
- const tooltip = initTooltip(targetElement, tooltipConfig, uuid)
87
-
88
- tooltips[uuid] = tooltip
89
-
90
- if (targetElement.matches(':hover')) {
91
- targetElement.dispatchEvent(new Event('mouseenter'))
92
- }
93
- }
94
-
95
- function getTooltipConfig(localConfig: string | TooltipLocalConfig, globalConfig?: TooltipConfig, position?: TooltipPosition) {
96
- // Tooltip config
97
- let appendTo = globalConfig?.appendTo ?? defaultAppendTo
98
- let tooltipText = getTooltipText(localConfig)
99
- let tooltipPosition = position ?? globalConfig?.defaultPosition ?? defaultTooltipPosition
100
- let tooltipPositions: TooltipPositions = {
101
- left: globalConfig?.positions?.left ?? defaultTooltipPositions.left,
102
- top: globalConfig?.positions?.top ?? defaultTooltipPositions.top,
103
- right: globalConfig?.positions?.right ?? defaultTooltipPositions.right,
104
- bottom: globalConfig?.positions?.bottom ?? defaultTooltipPositions.bottom,
105
- }
106
- let tooltipOffsetFromSource = globalConfig?.offsetFromSource ?? defaultTooltipOffsetFromSource
107
- let tooltipOffsetFromViewport = globalConfig?.offsetFromViewport ?? defaultTooltipOffsetFromViewport
108
- let tooltipMinWidth = globalConfig?.minWidth ?? defaultTooltipMinWidth
109
- let tooltipMaxWidth = globalConfig?.maxWidth ?? defaultTooltipMaxWidth
110
- let tooltipBorderWidth = globalConfig?.tooltipBorderWidth ?? defaultTooltipBorderWidth
111
- let tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + (globalConfig?.tooltipClasses ?? '')
112
- let textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + (globalConfig?.textClasses ?? '')
113
- let arrowSize = globalConfig?.arrowSize ?? defaultArrowSize
114
- let arrowClasses = globalConfig?.arrowClasses ?? ''
115
- let arrowMinOffsetFromTooltipCorner = globalConfig?.arrowMinOffsetFromTooltipCorner ?? defaultMinArrowOffsetFromTooltipCorner
116
- let zIndex = globalConfig?.zIndex ?? defaultZIndex
117
- let shouldShow = defaultShouldShow
118
- let showDelay = globalConfig?.showDelay ?? defaultShowDelay
119
- let hideDelay = globalConfig?.hideDelay ?? defaultHideDelay
120
-
121
- // Check if local config is defined (it's defined when local config is Object and not a string, because string means that just Tooltip text is given)
122
- if (typeof(localConfig) !== 'string') {
123
- if (localConfig.appendTo !== undefined) appendTo = localConfig.appendTo
124
- if (position === undefined && localConfig.defaultPosition !== undefined) tooltipPosition = localConfig.defaultPosition
125
-
126
- if (localConfig.positions?.left !== undefined) tooltipPositions.left = localConfig.positions.left
127
- if (localConfig.positions?.top !== undefined) tooltipPositions.top = localConfig.positions.top
128
- if (localConfig.positions?.right !== undefined) tooltipPositions.right = localConfig.positions.right
129
- if (localConfig.positions?.bottom !== undefined) tooltipPositions.bottom = localConfig.positions.bottom
130
-
131
- if (localConfig.offsetFromSource !== undefined) tooltipOffsetFromSource = localConfig.offsetFromSource
132
- if (localConfig.offsetFromViewport !== undefined) tooltipOffsetFromViewport = localConfig.offsetFromViewport
133
- if (localConfig.minWidth !== undefined) tooltipMinWidth = localConfig.minWidth
134
- if (localConfig.maxWidth !== undefined) tooltipMaxWidth = localConfig.maxWidth
135
- if (localConfig.tooltipBorderWidth !== undefined) tooltipBorderWidth = localConfig.tooltipBorderWidth
136
- if (localConfig.tooltipClasses !== undefined) tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + localConfig.tooltipClasses
137
- if (localConfig.textClasses !== undefined) textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + localConfig.textClasses
138
- if (localConfig.arrowSize !== undefined) arrowSize = localConfig.arrowSize
139
- if (localConfig.arrowClasses !== undefined) arrowClasses = localConfig.arrowClasses
140
- if (localConfig.arrowMinOffsetFromTooltipCorner !== undefined) arrowMinOffsetFromTooltipCorner = localConfig.arrowMinOffsetFromTooltipCorner
141
- if (localConfig.zIndex !== undefined) zIndex = localConfig.zIndex
142
- if (localConfig.show !== undefined) shouldShow = localConfig.show
143
- if (localConfig.showDelay !== undefined) showDelay = localConfig.showDelay
144
- if (localConfig.hideDelay !== undefined) hideDelay = localConfig.hideDelay
145
- }
146
-
147
- return {
148
- appendTo,
149
- tooltipText,
150
- tooltipPosition,
151
- tooltipPositions,
152
- tooltipOffsetFromSource,
153
- tooltipOffsetFromViewport,
154
- tooltipMinWidth,
155
- tooltipMaxWidth,
156
- tooltipBorderWidth,
157
- tooltipClasses,
158
- textClasses,
159
- arrowSize,
160
- arrowClasses,
161
- arrowMinOffsetFromTooltipCorner,
162
- zIndex,
163
- shouldShow,
164
- showDelay,
165
- hideDelay,
166
- }
167
- }
168
-
169
- function getTooltipText(localConfig: string | TooltipLocalConfig) {
170
- const tooltipText = typeof(localConfig) === 'string' ? localConfig : localConfig.content
171
-
172
- if (!tooltipText) {
173
- throw new Error("Please enter valid tooltip value");
174
- }
175
-
176
- return tooltipText
177
- }
178
-
179
- function initTooltip(targetElement: HTMLElement, tooltipConfig: ReturnType<typeof getTooltipConfig>, uuid: string) {
180
- let anchorElement = targetElement
181
-
182
- let tooltipTextElement = createTextElement(tooltipConfig.textClasses, tooltipConfig.tooltipText)
183
- let tooltipElement = createTooltipElement(tooltipConfig.tooltipClasses, tooltipConfig.tooltipBorderWidth)
184
- tooltipElement.append(tooltipTextElement)
185
- tooltipElement.dataset.uuid = uuid
186
-
187
- const mouseEventState = {
188
- currentInstanceId: Date.now(),
189
- isHoveringOverAnchorElement: false,
190
- lastTooltipMouseLeaveTimestamp: 0,
191
- }
192
-
193
- const mouseEnterEventControllers = {
194
- anchorElementMouseEnter: new AbortController(),
195
- anchorElementMouseLeave: new AbortController(),
196
- tooltipElementMouseEnter: new AbortController(),
197
- tooltipElementMouseLeave: new AbortController(),
198
- }
199
-
200
- anchorElement.addEventListener('mouseenter', () => onMouseEnter(anchorElement, tooltipConfig, tooltipElement, uuid), { signal: mouseEnterEventControllers.anchorElementMouseEnter.signal })
201
- anchorElement.addEventListener('mouseleave', () => onMouseLeave(tooltipConfig, uuid), { signal: mouseEnterEventControllers.anchorElementMouseLeave.signal })
202
-
203
- tooltipElement.addEventListener('mouseenter', () => onMouseEnter(anchorElement, tooltipConfig, tooltipElement, uuid, { isTooltip: true }), { signal: mouseEnterEventControllers.tooltipElementMouseEnter.signal })
204
- tooltipElement.addEventListener('mouseleave', () => onMouseLeave(tooltipConfig, uuid, { isTooltip: true }), { signal: mouseEnterEventControllers.tooltipElementMouseLeave.signal })
205
-
206
- return {
207
- anchorElement,
208
- tooltipConfig,
209
- tooltipElement,
210
- mouseEnterEventControllers,
211
- mouseEventState,
212
- }
213
- }
214
-
215
- function createTextElement(textClasses: string, tooltipText: string) {
216
- let tooltipTextElement = document.createElement('p')
217
- tooltipTextElement.classList.add(...textClasses.trim().split(' '))
218
- tooltipTextElement.innerHTML = tooltipText
219
-
220
- return tooltipTextElement
221
- }
222
-
223
- function createTooltipElement(tooltipClasses: string, tooltipBorderWidth: number) {
224
- let tooltipElement = document.createElement('div')
225
- tooltipElement.classList.add(...tooltipClasses.trim().split(' '))
226
- tooltipElement.style.borderWidth = `${tooltipBorderWidth}px`
227
-
228
- return tooltipElement
229
- }
230
-
231
- async function onMouseEnter(
232
- anchorElement: HTMLElement,
233
- tooltipConfig: ReturnType<typeof getTooltipConfig>,
234
- tooltipElement: HTMLDivElement,
235
- uuid: string,
236
- options?: { isTooltip?: boolean }
237
- ) {
238
- if (!tooltipConfig.shouldShow) return
239
-
240
- let _showDelay = options?.isTooltip ? 0 : tooltipConfig.showDelay
241
-
242
- // If mouse leaves from Tooltip and enters to Anchor element in short time, show Tooltip immediately
243
- const mouseLeaveFromTooltipBufferTime = 100
244
- if (!options?.isTooltip && Date.now() - tooltips[uuid].mouseEventState.lastTooltipMouseLeaveTimestamp <= mouseLeaveFromTooltipBufferTime) {
245
- _showDelay = 0
246
- }
247
-
248
- const currentInstanceId = Date.now()
249
- tooltips[uuid].mouseEventState.currentInstanceId = currentInstanceId
250
- tooltips[uuid].mouseEventState.isHoveringOverAnchorElement = true
251
-
252
- if (_showDelay > 0) {
253
- await new Promise(resolve => setTimeout(resolve, _showDelay))
254
-
255
- if (!tooltips[uuid].mouseEventState.isHoveringOverAnchorElement || tooltips[uuid].mouseEventState.currentInstanceId !== currentInstanceId) return
256
- }
257
-
258
- const anchorElementRect = anchorElement.getBoundingClientRect()
259
-
260
- // Mount Tooltip element to target element (default is `body`)
261
- const appendToTarget = document.querySelector(tooltipConfig.appendTo)
262
- appendToTarget?.appendChild(tooltipElement)
263
-
264
- // Find suitable Tooltip position
265
- let hasNeededDisplaySpace = false
266
- let currentTooltipPosition = tooltipConfig.tooltipPosition
267
- for (let i = 0; i < 4; i++) {
268
- currentTooltipPosition = tooltipConfig.tooltipPositions[tooltipConfig.tooltipPosition][i]
269
-
270
- if (currentTooltipPosition === 'left') {
271
- hasNeededDisplaySpace = tryMountTooltipOnLeft(anchorElementRect, tooltipConfig, tooltipElement)
272
- } else if (currentTooltipPosition === 'top') {
273
- hasNeededDisplaySpace = tryMountTooltipOnTop(anchorElementRect, tooltipConfig, tooltipElement)
274
- } else if (currentTooltipPosition === 'right') {
275
- hasNeededDisplaySpace = tryMountTooltipOnRight(anchorElementRect, tooltipConfig, tooltipElement)
276
- } else if (currentTooltipPosition === 'bottom') {
277
- hasNeededDisplaySpace = tryMountTooltipOnBottom(anchorElementRect, tooltipConfig, tooltipElement)
278
- }
279
-
280
- if (hasNeededDisplaySpace) break
281
- }
282
-
283
- if (hasNeededDisplaySpace) {
284
- drawArrow(anchorElementRect, currentTooltipPosition, tooltipConfig, tooltipElement)
285
-
286
- tooltipElement.style.opacity = '1'
287
- tooltipElement.style.zIndex = typeof(tooltipConfig.zIndex) === 'string' ? tooltipConfig.zIndex : tooltipConfig.zIndex.toString();
288
-
289
- handleHideOnScroll(anchorElement, () => hideTooltip(uuid))
290
- handleHideOnResize(uuid, anchorElement, () => hideTooltip(uuid))
291
- }
292
- }
293
-
294
- async function onMouseLeave(tooltipConfig: ReturnType<typeof getTooltipConfig>, uuid: string, options?: { isTooltip?: boolean }) {
295
- if (options?.isTooltip) {
296
- tooltips[uuid].mouseEventState.lastTooltipMouseLeaveTimestamp = Date.now()
297
- }
298
-
299
- const currentInstanceId = Date.now()
300
- tooltips[uuid].mouseEventState.currentInstanceId = currentInstanceId
301
- tooltips[uuid].mouseEventState.isHoveringOverAnchorElement = false
302
-
303
- if (tooltipConfig.hideDelay > 0) {
304
- await new Promise(resolve => setTimeout(resolve, tooltipConfig.hideDelay))
305
-
306
- if (tooltips[uuid].mouseEventState.isHoveringOverAnchorElement || tooltips[uuid].mouseEventState.currentInstanceId !== currentInstanceId) return
307
- }
308
-
309
- hideTooltip(uuid)
310
- }
311
-
312
- function tryMountTooltipOnLeft(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
313
- // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
314
- const tooltipAvailableMaxWidth = Math.min(anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
315
- const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
316
- const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
317
-
318
- if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
319
-
320
- // Set Tooltip maxWidth
321
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
322
-
323
- // Calculate Tooltip position
324
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
325
- let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
326
-
327
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
328
- tooltipTop = tooltipConfig.tooltipOffsetFromViewport
329
- } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
330
- tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
331
- }
332
-
333
- const tooltipLeft = anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.width
334
-
335
- // Check if anchor element is directly on right of Tooltip
336
- if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
337
- || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
338
-
339
- // Set Tooltip position
340
- tooltipElement.style.top = `${tooltipTop}px`
341
- tooltipElement.style.left = `${tooltipLeft}px`
342
-
343
- return true
344
- }
345
-
346
- function tryMountTooltipOnRight(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
347
- // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
348
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource) - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
349
- const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
350
- const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
351
-
352
- if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
353
-
354
- // Set tooltip maxWidth
355
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
356
-
357
- // Calculate Tooltip position
358
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
359
-
360
- let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
361
-
362
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
363
- tooltipTop = tooltipConfig.tooltipOffsetFromViewport
364
- } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
365
- tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
366
- }
367
-
368
- const tooltipLeft = anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource
369
-
370
- // Check if anchor element is directly on left of Tooltip
371
- if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
372
- || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
373
-
374
- // Set Tooltip position
375
- tooltipElement.style.top = `${tooltipTop}px`
376
- tooltipElement.style.left = `${tooltipLeft}px`
377
-
378
- return true
379
- }
380
-
381
- function tryMountTooltipOnTop(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
382
- // Calculate and set Tooltip width
383
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
384
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
385
-
386
- // Calculate Tooltip top position
387
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
388
- let tooltipTop = anchorElementRect.top - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.height
389
-
390
- // Check if Tooltip has enough available on top
391
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) return false
392
-
393
- // Calculate Tooltip left position
394
- let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
395
-
396
- if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
397
- tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
398
- } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
399
- tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
400
- }
401
-
402
- // Check if anchor element is directly on below of Tooltip
403
- if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
404
- || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
405
-
406
- // Set Tooltip position
407
- tooltipElement.style.top = `${tooltipTop}px`
408
- tooltipElement.style.left = `${tooltipLeft}px`
409
-
410
- return true
411
- }
412
-
413
- function tryMountTooltipOnBottom(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
414
- // Calculate and set Tooltip width
415
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
416
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
417
-
418
- // Calculate Tooltip top position
419
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
420
- let tooltipTop = anchorElementRect.bottom + tooltipConfig.tooltipOffsetFromSource
421
-
422
- // Check if Tooltip has enough available on bottom
423
- if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) return false
424
-
425
- // Calculate Tooltip left position
426
- let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
427
-
428
- if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
429
- tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
430
- } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
431
- tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
432
- }
433
-
434
- // Check if anchor element is directly on top of Tooltip
435
- if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
436
- || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
437
-
438
- // Set Tooltip position
439
- tooltipElement.style.top = `${tooltipTop}px`
440
- tooltipElement.style.left = `${tooltipLeft}px`
441
-
442
- return true
443
- }
444
-
445
- function drawArrow(anchorElementRect: DOMRect, currentTooltipPosition: TooltipPosition, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
446
- // Create Arrow element
447
- const arrowElement = document.createElement('div')
448
-
449
- // Calculate Arrow element size, positions and style/angle classes
450
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
451
- const arrowHalfLengthOfLongSide = Math.sin(45 * (180 / Math.PI)) * tooltipConfig.arrowSize
452
-
453
- // Adjusts arrow position by `x` pixels to handle browsers sometimes not rendering border in it's full width, e.g., 4.8px instead of 5px
454
- const arrowPositionAdjuster = 1;
455
-
456
- // Arrow top/left 0 is Tooltip top/left 0
457
- let arrowTop = 0
458
- let arrowLeft = 0
459
-
460
- let arrowClassForCorrectAngle = ''
461
-
462
- switch (currentTooltipPosition) {
463
- case "left":
464
- arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-r-transparent'
465
- arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
466
- arrowLeft = tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
467
- break;
468
- case "top":
469
- arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-b-transparent'
470
- arrowTop = tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
471
- arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
472
- break;
473
- case "right":
474
- arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-l-transparent'
475
- arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
476
- arrowLeft = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
477
- break;
478
- case "bottom":
479
- arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-t-transparent'
480
- arrowTop = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
481
- arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
482
- break;
483
- }
484
-
485
- if (currentTooltipPosition === 'left' || currentTooltipPosition === 'right') {
486
- if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)) {
487
- arrowTop = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)
488
- }
489
- } else {
490
- if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)) {
491
- arrowLeft = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)
492
- }
493
- }
494
-
495
- // Set Arrow element id, styling/angle
496
- const adjustedArrowClasses = arrowElementClass + ' ' + defaultArrowClasses + ' ' + arrowClassForCorrectAngle + ' ' + tooltipConfig.arrowClasses
497
-
498
- arrowElement.classList.add(...adjustedArrowClasses.trim().split(' '))
499
-
500
- // Set Arrow element size and position
501
- arrowElement.style.top = `${arrowTop}px`
502
- arrowElement.style.left = `${arrowLeft}px`
503
- arrowElement.style.borderWidth = `${tooltipConfig.arrowSize}px`
504
-
505
- // Mount Arrow element
506
- tooltipElement.appendChild(arrowElement)
507
- }
508
-
509
- function isArrowPositionWithinLimits(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
510
- switch (currentTooltipPosition) {
511
- case "left":
512
- case "right":
513
- return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
514
- && arrowPosition < tooltipElementRect.height + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
515
- case "top":
516
- case "bottom":
517
- return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
518
- && arrowPosition < tooltipElementRect.width + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
519
- }
520
- }
521
-
522
- function getArrowPositionMinLimit(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
523
- switch (currentTooltipPosition) {
524
- case "left":
525
- case "right":
526
- if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
527
- // Arrow too close to viewport top
528
- return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
529
- } else {
530
- // Arrow too close to viewport bottom
531
- return tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
532
- }
533
- case "top":
534
- case "bottom":
535
- if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
536
- // Arrow too close to viewport left
537
- return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
538
- } else {
539
- // Arrow too close to viewport right
540
- return tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
541
- }
542
- }
543
- }
544
-
545
- function hideTooltip(uuid: string) {
546
- const tooltipElement = tooltips[uuid]?.tooltipElement
547
-
548
- resetResizeReferences(uuid)
549
-
550
- // Remove Arrow element from Tooltip, because it needs to be rebuilt every time Tooltip is showed again
551
- tooltipElement.querySelector(`.${arrowElementClass}`)?.remove()
552
-
553
- // Reset position so that old position does not effect new position (when zooming old position could be off screen)
554
- tooltipElement.style.left = '0'
555
- tooltipElement.style.top = '0'
556
-
557
- tooltipElement.remove()
558
- }
559
-
560
- function destroyTooltip(tooltip: ReturnType<typeof initTooltip>) {
561
- const uuid = tooltip.tooltipElement.dataset.uuid
562
-
563
- if (uuid) {
564
- hideTooltip(uuid)
565
- delete tooltips[uuid]
566
- }
567
-
568
- for (const controller of Object.values(tooltip.mouseEnterEventControllers)) {
569
- controller.abort()
570
- }
571
- }
572
-
573
- export default ZeroTooltip
1
+ import { Directive, isReactive, watch } from "vue"
2
+ import { v4 as uuidv4 } from 'uuid'
3
+ import TooltipConfig from "./types/tooltipConfig"
4
+ import TooltipPosition from "./types/tooltipPosition"
5
+ import TooltipPositions from "./types/tooltipPositions"
6
+ import TooltipLocalConfig from "./types/tooltipLocalConfig"
7
+ import useHideOnScroll from './composables/useHideOnScroll'
8
+ import useHideOnResize from "./composables/useHideOnResize"
9
+ import useRepositionOnResize from "./composables/useRepositionOnResize"
10
+
11
+ const { handleHideOnScroll, removeHideOnScrollListeners } = useHideOnScroll()
12
+ const { handleHideOnResize, resetResizeReferences } = useHideOnResize()
13
+ const { handleRepositionOnResize, removeRepositionOnResizeHandler } = useRepositionOnResize()
14
+
15
+ const tooltipElementClass = 'zero-tooltip__container'
16
+ const textElementClass = 'zero-tooltip__text'
17
+ const arrowElementClass = 'zero-tooltip__arrow'
18
+
19
+ // For each TooltipPosition define sequence of positions that will be checked when determining where to render Tooltip
20
+ // Meant as fallback positions in case Tooltip do not have enough space in originally set position
21
+ const defaultTooltipPositions: TooltipPositions = {
22
+ left: ['left', 'right', 'top', 'bottom'],
23
+ top: ['top', 'bottom', 'right', 'left'],
24
+ right: ['right', 'left', 'top', 'bottom'],
25
+ bottom: ['bottom', 'top', 'right', 'left'],
26
+ }
27
+
28
+ const defaultAppendTo: string = 'body'
29
+ const defaultTooltipPosition: TooltipPosition = 'top'
30
+ const defaultTooltipOffsetFromSource = 10
31
+ const defaultTooltipOffsetFromViewport = 20
32
+ const defaultTooltipMinWidth = 100
33
+ const defaultTooltipMaxWidth = 250
34
+ const defaultTooltipBorderWidth = 0
35
+ const defaultTooltipClasses = 'zt-fixed zt-opacity-0 zt-inline-block zt-w-fit zt-py-1.5 zt-px-2.5 zt-rounded-md zt-bg-[#495057] zt-shadow-[0_2px_12px_0_rgba(0,0,0,0.1)] zt-box-border'
36
+ const defaultTextClasses = 'zt-text-sm zt-text-white zt-whitespace-pre-wrap zt-break-words'
37
+ const defaultArrowSize = 5
38
+ const defaultArrowClasses = 'zt-absolute zt-border-solid zt-border-[#495057]'
39
+ const defaultMinArrowOffsetFromTooltipCorner = 6
40
+ const defaultZIndex = 1
41
+ const defaultShouldShow = true
42
+ const defaultShowDelay = 0
43
+ const defaultHideDelay = 0
44
+ const defaultAlwaysOn = false
45
+
46
+ const tooltips: {[key: string]: ReturnType<typeof initTooltip>} = {}
47
+
48
+ const ZeroTooltip = (globalConfig?: TooltipConfig): Directive => {
49
+ return {
50
+ created: (targetElement: HTMLElement, binding, vnode) => {
51
+ const uuid = uuidv4()
52
+ vnode.el.$_tooltip = { uuid: uuid }
53
+
54
+ buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
55
+
56
+ if (typeof(binding.value) !== 'string' && isReactive(binding.value)) {
57
+ watch(binding.value, (newBindingValue) => {
58
+ if (tooltips[uuid]) {
59
+ destroyTooltip(tooltips[uuid])
60
+ }
61
+
62
+ buildTooltip(newBindingValue, globalConfig, binding.arg, targetElement, uuid)
63
+ })
64
+ }
65
+ },
66
+
67
+ updated: (targetElement: HTMLElement, binding, vnode) => {
68
+ const uuid = vnode.el.$_tooltip.uuid
69
+
70
+ if (tooltips[uuid]) {
71
+ destroyTooltip(tooltips[uuid])
72
+ }
73
+
74
+ buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
75
+ },
76
+
77
+ beforeUnmount: (_, __, vnode) => {
78
+ const uuid = vnode.el.$_tooltip.uuid
79
+
80
+ if (tooltips[uuid]) {
81
+ destroyTooltip(tooltips[uuid])
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ function buildTooltip(bindingValue: any, globalConfig: TooltipConfig | undefined, bindingArgument: string | undefined, targetElement: HTMLElement, uuid: string) {
88
+ let tooltipConfig = getTooltipConfig(bindingValue as string | TooltipLocalConfig, globalConfig, bindingArgument as TooltipPosition)
89
+ const tooltip = initTooltip(targetElement, tooltipConfig, uuid)
90
+
91
+ tooltips[uuid] = tooltip
92
+
93
+ if (targetElement.matches(':hover')) {
94
+ targetElement.dispatchEvent(new Event('mouseenter'))
95
+ }
96
+ }
97
+
98
+ function getTooltipConfig(localConfig: string | TooltipLocalConfig, globalConfig?: TooltipConfig, position?: TooltipPosition) {
99
+ // Tooltip config
100
+ let appendTo = globalConfig?.appendTo ?? defaultAppendTo
101
+ let tooltipText = getTooltipText(localConfig)
102
+ let tooltipPosition = position ?? globalConfig?.defaultPosition ?? defaultTooltipPosition
103
+ let tooltipPositions: TooltipPositions = {
104
+ left: globalConfig?.positions?.left ?? defaultTooltipPositions.left,
105
+ top: globalConfig?.positions?.top ?? defaultTooltipPositions.top,
106
+ right: globalConfig?.positions?.right ?? defaultTooltipPositions.right,
107
+ bottom: globalConfig?.positions?.bottom ?? defaultTooltipPositions.bottom,
108
+ }
109
+ let tooltipOffsetFromSource = globalConfig?.offsetFromSource ?? defaultTooltipOffsetFromSource
110
+ let tooltipOffsetFromViewport = globalConfig?.offsetFromViewport ?? defaultTooltipOffsetFromViewport
111
+ let tooltipMinWidth = globalConfig?.minWidth ?? defaultTooltipMinWidth
112
+ let tooltipMaxWidth = globalConfig?.maxWidth ?? defaultTooltipMaxWidth
113
+ let tooltipBorderWidth = globalConfig?.tooltipBorderWidth ?? defaultTooltipBorderWidth
114
+ let tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + (globalConfig?.tooltipClasses ?? '')
115
+ let textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + (globalConfig?.textClasses ?? '')
116
+ let arrowSize = globalConfig?.arrowSize ?? defaultArrowSize
117
+ let arrowClasses = globalConfig?.arrowClasses ?? ''
118
+ let arrowMinOffsetFromTooltipCorner = globalConfig?.arrowMinOffsetFromTooltipCorner ?? defaultMinArrowOffsetFromTooltipCorner
119
+ let zIndex = globalConfig?.zIndex ?? defaultZIndex
120
+ let shouldShow = defaultShouldShow
121
+ let showDelay = globalConfig?.showDelay ?? defaultShowDelay
122
+ let hideDelay = globalConfig?.hideDelay ?? defaultHideDelay
123
+ let alwaysOn = defaultAlwaysOn
124
+
125
+ // Check if local config is defined (it's defined when local config is Object and not a string, because string means that just Tooltip text is given)
126
+ if (typeof(localConfig) !== 'string') {
127
+ if (localConfig.appendTo !== undefined) appendTo = localConfig.appendTo
128
+ if (position === undefined && localConfig.defaultPosition !== undefined) tooltipPosition = localConfig.defaultPosition
129
+
130
+ if (localConfig.positions?.left !== undefined) tooltipPositions.left = localConfig.positions.left
131
+ if (localConfig.positions?.top !== undefined) tooltipPositions.top = localConfig.positions.top
132
+ if (localConfig.positions?.right !== undefined) tooltipPositions.right = localConfig.positions.right
133
+ if (localConfig.positions?.bottom !== undefined) tooltipPositions.bottom = localConfig.positions.bottom
134
+
135
+ if (localConfig.offsetFromSource !== undefined) tooltipOffsetFromSource = localConfig.offsetFromSource
136
+ if (localConfig.offsetFromViewport !== undefined) tooltipOffsetFromViewport = localConfig.offsetFromViewport
137
+ if (localConfig.minWidth !== undefined) tooltipMinWidth = localConfig.minWidth
138
+ if (localConfig.maxWidth !== undefined) tooltipMaxWidth = localConfig.maxWidth
139
+ if (localConfig.tooltipBorderWidth !== undefined) tooltipBorderWidth = localConfig.tooltipBorderWidth
140
+ if (localConfig.tooltipClasses !== undefined) tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + localConfig.tooltipClasses
141
+ if (localConfig.textClasses !== undefined) textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + localConfig.textClasses
142
+ if (localConfig.arrowSize !== undefined) arrowSize = localConfig.arrowSize
143
+ if (localConfig.arrowClasses !== undefined) arrowClasses = localConfig.arrowClasses
144
+ if (localConfig.arrowMinOffsetFromTooltipCorner !== undefined) arrowMinOffsetFromTooltipCorner = localConfig.arrowMinOffsetFromTooltipCorner
145
+ if (localConfig.zIndex !== undefined) zIndex = localConfig.zIndex
146
+ if (localConfig.show !== undefined) shouldShow = localConfig.show
147
+ if (localConfig.showDelay !== undefined) showDelay = localConfig.showDelay
148
+ if (localConfig.hideDelay !== undefined) hideDelay = localConfig.hideDelay
149
+ if (localConfig.alwaysOn !== undefined) alwaysOn = localConfig.alwaysOn
150
+ }
151
+
152
+ return {
153
+ appendTo,
154
+ tooltipText,
155
+ tooltipPosition,
156
+ tooltipPositions,
157
+ tooltipOffsetFromSource,
158
+ tooltipOffsetFromViewport,
159
+ tooltipMinWidth,
160
+ tooltipMaxWidth,
161
+ tooltipBorderWidth,
162
+ tooltipClasses,
163
+ textClasses,
164
+ arrowSize,
165
+ arrowClasses,
166
+ arrowMinOffsetFromTooltipCorner,
167
+ zIndex,
168
+ shouldShow,
169
+ showDelay,
170
+ hideDelay,
171
+ alwaysOn
172
+ }
173
+ }
174
+
175
+ function getTooltipText(localConfig: string | TooltipLocalConfig) {
176
+ const tooltipText = typeof(localConfig) === 'string' ? localConfig : localConfig.content
177
+
178
+ if (typeof(localConfig) === 'string' && tooltipText.trim() === '') {
179
+ throw new Error("Tooltip text must not be empty string OR set option 'show' to false")
180
+ }
181
+
182
+ if (typeof(localConfig) !== 'string' && tooltipText.trim() === '' && localConfig.show !== false) {
183
+ throw new Error("Tooltip 'content' must not be empty string OR set option 'show' to false")
184
+ }
185
+
186
+ return tooltipText
187
+ }
188
+
189
+ function initTooltip(targetElement: HTMLElement, tooltipConfig: ReturnType<typeof getTooltipConfig>, uuid: string) {
190
+ let anchorElement = targetElement
191
+
192
+ // Create Tooltip element
193
+ let tooltipTextElement = createTextElement(tooltipConfig.textClasses, tooltipConfig.tooltipText)
194
+ let tooltipElement = createTooltipContainerElement(tooltipConfig.tooltipClasses, tooltipConfig.tooltipBorderWidth)
195
+ tooltipElement.append(tooltipTextElement)
196
+ tooltipElement.dataset.uuid = uuid
197
+
198
+ const mouseEventState = {
199
+ currentInstanceId: Date.now(),
200
+ isHoveringOverAnchorElement: false,
201
+ lastTooltipMouseLeaveTimestamp: 0,
202
+ }
203
+
204
+ const mouseEnterEventControllers = {
205
+ anchorElementMouseEnter: new AbortController(),
206
+ anchorElementMouseLeave: new AbortController(),
207
+ tooltipElementMouseEnter: new AbortController(),
208
+ tooltipElementMouseLeave: new AbortController(),
209
+ }
210
+
211
+ if (!tooltipConfig.alwaysOn) {
212
+ anchorElement.addEventListener('mouseenter', () => onMouseEnter(anchorElement, tooltipConfig, tooltipElement, uuid), { signal: mouseEnterEventControllers.anchorElementMouseEnter.signal })
213
+ anchorElement.addEventListener('mouseleave', () => onMouseLeave(tooltipConfig, uuid), { signal: mouseEnterEventControllers.anchorElementMouseLeave.signal })
214
+
215
+ tooltipElement.addEventListener('mouseenter', () => onMouseEnter(anchorElement, tooltipConfig, tooltipElement, uuid, { isTooltip: true }), { signal: mouseEnterEventControllers.tooltipElementMouseEnter.signal })
216
+ tooltipElement.addEventListener('mouseleave', () => onMouseLeave(tooltipConfig, uuid, { isTooltip: true }), { signal: mouseEnterEventControllers.tooltipElementMouseLeave.signal })
217
+ } else {
218
+ setTimeout(() => {
219
+ mountTooltipElement(anchorElement, tooltipConfig, tooltipElement, 'absolute')
220
+ handleRepositionOnResize(uuid, () => repositionTooltipElement(anchorElement, tooltipConfig, tooltipElement, 'absolute'))
221
+ }, 0)
222
+ }
223
+
224
+ return {
225
+ anchorElement,
226
+ tooltipConfig,
227
+ tooltipElement,
228
+ mouseEnterEventControllers,
229
+ mouseEventState,
230
+ }
231
+ }
232
+
233
+ function createTextElement(textClasses: string, tooltipText: string) {
234
+ let tooltipTextElement = document.createElement('p')
235
+ tooltipTextElement.classList.add(...textClasses.trim().split(' '))
236
+ tooltipTextElement.innerHTML = tooltipText
237
+
238
+ return tooltipTextElement
239
+ }
240
+
241
+ function createTooltipContainerElement(tooltipClasses: string, tooltipBorderWidth: number) {
242
+ let tooltipElement = document.createElement('div')
243
+ tooltipElement.classList.add(...tooltipClasses.trim().split(' '))
244
+ tooltipElement.style.borderWidth = `${tooltipBorderWidth}px`
245
+
246
+ return tooltipElement
247
+ }
248
+
249
+ async function onMouseEnter(
250
+ anchorElement: HTMLElement,
251
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
252
+ tooltipElement: HTMLDivElement,
253
+ uuid: string,
254
+ options?: { isTooltip?: boolean }
255
+ ) {
256
+ if (!tooltipConfig.shouldShow) return
257
+
258
+ let _showDelay = options?.isTooltip ? 0 : tooltipConfig.showDelay
259
+
260
+ // If mouse leaves from Tooltip and enters to Anchor element in short time, show Tooltip immediately
261
+ const mouseLeaveFromTooltipBufferTime = 100
262
+ if (!options?.isTooltip && Date.now() - tooltips[uuid].mouseEventState.lastTooltipMouseLeaveTimestamp <= mouseLeaveFromTooltipBufferTime) {
263
+ _showDelay = 0
264
+ }
265
+
266
+ const currentInstanceId = Date.now()
267
+ tooltips[uuid].mouseEventState.currentInstanceId = currentInstanceId
268
+ tooltips[uuid].mouseEventState.isHoveringOverAnchorElement = true
269
+
270
+ if (_showDelay > 0) {
271
+ await new Promise(resolve => setTimeout(resolve, _showDelay))
272
+
273
+ if (!tooltips[uuid].mouseEventState.isHoveringOverAnchorElement || tooltips[uuid].mouseEventState.currentInstanceId !== currentInstanceId) return
274
+ }
275
+
276
+ const didMountTooltip = mountTooltipElement(anchorElement, tooltipConfig, tooltipElement)
277
+
278
+ if (didMountTooltip) {
279
+ handleHideOnScroll(anchorElement, () => hideTooltip(uuid))
280
+ handleHideOnResize(uuid, anchorElement, () => hideTooltip(uuid))
281
+ }
282
+ }
283
+
284
+ function mountTooltipElement(
285
+ anchorElement: HTMLElement,
286
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
287
+ tooltipElement: HTMLDivElement,
288
+ positionStrategy?: 'fixed' | 'absolute'
289
+ ) {
290
+ let scrollOffset = { x: 0, y: 0 }
291
+
292
+ if (positionStrategy === 'absolute') {
293
+ tooltipElement.classList.replace('zt-fixed', 'zt-absolute')
294
+ scrollOffset.x = window.scrollX
295
+ scrollOffset.y = window.scrollY
296
+ }
297
+
298
+ const anchorElementRect = anchorElement.getBoundingClientRect()
299
+
300
+ // Mount Tooltip element to target element (default is `body`)
301
+ const appendToTarget = document.querySelector(tooltipConfig.appendTo)
302
+ appendToTarget?.appendChild(tooltipElement)
303
+
304
+ // Find suitable Tooltip position
305
+ let hasNeededDisplaySpace = false
306
+ let currentTooltipPosition = tooltipConfig.tooltipPosition
307
+ for (let i = 0; i < 4; i++) {
308
+ currentTooltipPosition = tooltipConfig.tooltipPositions[tooltipConfig.tooltipPosition][i]
309
+
310
+ if (currentTooltipPosition === 'left') {
311
+ hasNeededDisplaySpace = tryMountTooltipOnLeft(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
312
+ } else if (currentTooltipPosition === 'top') {
313
+ hasNeededDisplaySpace = tryMountTooltipOnTop(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
314
+ } else if (currentTooltipPosition === 'right') {
315
+ hasNeededDisplaySpace = tryMountTooltipOnRight(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
316
+ } else if (currentTooltipPosition === 'bottom') {
317
+ hasNeededDisplaySpace = tryMountTooltipOnBottom(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
318
+ }
319
+
320
+ if (hasNeededDisplaySpace) break
321
+ }
322
+
323
+ if (hasNeededDisplaySpace) {
324
+ drawArrow(anchorElementRect, currentTooltipPosition, tooltipConfig, tooltipElement)
325
+
326
+ tooltipElement.style.opacity = '1'
327
+ tooltipElement.style.zIndex = typeof(tooltipConfig.zIndex) === 'string' ? tooltipConfig.zIndex : tooltipConfig.zIndex.toString();
328
+
329
+ return true
330
+ }
331
+
332
+ return false
333
+ }
334
+
335
+ function repositionTooltipElement(
336
+ anchorElement: HTMLElement,
337
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
338
+ tooltipElement: HTMLDivElement,
339
+ positionStrategy?: 'fixed' | 'absolute'
340
+ ) {
341
+ // Remove Arrow element from Tooltip, because it needs to be rebuilt every time Tooltip is repositioned
342
+ tooltipElement.querySelector(`.${arrowElementClass}`)?.remove()
343
+
344
+ let scrollOffset = { x: 0, y: 0 }
345
+
346
+ if (positionStrategy === 'absolute') {
347
+ scrollOffset.x = window.scrollX
348
+ scrollOffset.y = window.scrollY
349
+ }
350
+
351
+ const anchorElementRect = anchorElement.getBoundingClientRect()
352
+
353
+ // Find suitable Tooltip position
354
+ let hasNeededDisplaySpace = false
355
+ let currentTooltipPosition = tooltipConfig.tooltipPosition
356
+ for (let i = 0; i < 4; i++) {
357
+ currentTooltipPosition = tooltipConfig.tooltipPositions[tooltipConfig.tooltipPosition][i]
358
+
359
+ if (currentTooltipPosition === 'left') {
360
+ hasNeededDisplaySpace = tryMountTooltipOnLeft(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
361
+ } else if (currentTooltipPosition === 'top') {
362
+ hasNeededDisplaySpace = tryMountTooltipOnTop(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
363
+ } else if (currentTooltipPosition === 'right') {
364
+ hasNeededDisplaySpace = tryMountTooltipOnRight(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
365
+ } else if (currentTooltipPosition === 'bottom') {
366
+ hasNeededDisplaySpace = tryMountTooltipOnBottom(anchorElementRect, tooltipConfig, tooltipElement, scrollOffset)
367
+ }
368
+
369
+ if (hasNeededDisplaySpace) break
370
+ }
371
+
372
+ if (hasNeededDisplaySpace) {
373
+ drawArrow(anchorElementRect, currentTooltipPosition, tooltipConfig, tooltipElement)
374
+ }
375
+ }
376
+
377
+ async function onMouseLeave(tooltipConfig: ReturnType<typeof getTooltipConfig>, uuid: string, options?: { isTooltip?: boolean }) {
378
+ if (options?.isTooltip) {
379
+ tooltips[uuid].mouseEventState.lastTooltipMouseLeaveTimestamp = Date.now()
380
+ }
381
+
382
+ const currentInstanceId = Date.now()
383
+ tooltips[uuid].mouseEventState.currentInstanceId = currentInstanceId
384
+ tooltips[uuid].mouseEventState.isHoveringOverAnchorElement = false
385
+
386
+ if (tooltipConfig.hideDelay > 0) {
387
+ await new Promise(resolve => setTimeout(resolve, tooltipConfig.hideDelay))
388
+
389
+ if (tooltips[uuid].mouseEventState.isHoveringOverAnchorElement || tooltips[uuid].mouseEventState.currentInstanceId !== currentInstanceId) return
390
+ }
391
+
392
+ hideTooltip(uuid)
393
+ }
394
+
395
+ function tryMountTooltipOnLeft(
396
+ anchorElementRect: DOMRect,
397
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
398
+ tooltipElement: HTMLDivElement,
399
+ scrollOffset: { x: number, y: number },
400
+ ) {
401
+ // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
402
+ const tooltipAvailableMaxWidth = Math.min(anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
403
+ const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
404
+ const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
405
+
406
+ if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
407
+
408
+ // Set Tooltip maxWidth
409
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
410
+
411
+ // Calculate Tooltip position
412
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
413
+ let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
414
+
415
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
416
+ tooltipTop = tooltipConfig.tooltipOffsetFromViewport
417
+ } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
418
+ tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
419
+ }
420
+
421
+ const tooltipLeft = anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.width
422
+
423
+ // Check if anchor element is directly on right of Tooltip
424
+ if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
425
+ || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
426
+
427
+ // Set Tooltip position
428
+ tooltipElement.style.top = `${tooltipTop + scrollOffset.y}px`
429
+ tooltipElement.style.left = `${tooltipLeft + scrollOffset.x}px`
430
+
431
+ return true
432
+ }
433
+
434
+ function tryMountTooltipOnRight(
435
+ anchorElementRect: DOMRect,
436
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
437
+ tooltipElement: HTMLDivElement,
438
+ scrollOffset: { x: number, y: number },
439
+ ) {
440
+ // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
441
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource) - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
442
+ const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
443
+ const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
444
+
445
+ if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
446
+
447
+ // Set tooltip maxWidth
448
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
449
+
450
+ // Calculate Tooltip position
451
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
452
+
453
+ let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
454
+
455
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
456
+ tooltipTop = tooltipConfig.tooltipOffsetFromViewport
457
+ } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
458
+ tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
459
+ }
460
+
461
+ const tooltipLeft = anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource
462
+
463
+ // Check if anchor element is directly on left of Tooltip
464
+ if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
465
+ || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
466
+
467
+ // Set Tooltip position
468
+ tooltipElement.style.top = `${tooltipTop + scrollOffset.y}px`
469
+ tooltipElement.style.left = `${tooltipLeft + scrollOffset.x}px`
470
+
471
+ return true
472
+ }
473
+
474
+ function tryMountTooltipOnTop(
475
+ anchorElementRect: DOMRect,
476
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
477
+ tooltipElement: HTMLDivElement,
478
+ scrollOffset: { x: number, y: number },
479
+ ) {
480
+ // Calculate and set Tooltip width
481
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
482
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
483
+
484
+ // Calculate Tooltip top position
485
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
486
+ let tooltipTop = anchorElementRect.top - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.height
487
+
488
+ // Check if Tooltip has enough available on top
489
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) return false
490
+
491
+ // Calculate Tooltip left position
492
+ let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
493
+
494
+ if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
495
+ tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
496
+ } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
497
+ tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
498
+ }
499
+
500
+ // Check if anchor element is directly on below of Tooltip
501
+ if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
502
+ || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
503
+
504
+ // Set Tooltip position
505
+ tooltipElement.style.top = `${tooltipTop + scrollOffset.y}px`
506
+ tooltipElement.style.left = `${tooltipLeft + scrollOffset.x}px`
507
+
508
+ return true
509
+ }
510
+
511
+ function tryMountTooltipOnBottom(
512
+ anchorElementRect: DOMRect,
513
+ tooltipConfig: ReturnType<typeof getTooltipConfig>,
514
+ tooltipElement: HTMLDivElement,
515
+ scrollOffset: { x: number, y: number },
516
+ ) {
517
+ // Calculate and set Tooltip width
518
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
519
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
520
+
521
+ // Calculate Tooltip top position
522
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
523
+ let tooltipTop = anchorElementRect.bottom + tooltipConfig.tooltipOffsetFromSource
524
+
525
+ // Check if Tooltip has enough available on bottom
526
+ if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) return false
527
+
528
+ // Calculate Tooltip left position
529
+ let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
530
+
531
+ if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
532
+ tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
533
+ } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
534
+ tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
535
+ }
536
+
537
+ // Check if anchor element is directly on top of Tooltip
538
+ if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
539
+ || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
540
+
541
+ // Set Tooltip position
542
+ tooltipElement.style.top = `${tooltipTop + scrollOffset.y}px`
543
+ tooltipElement.style.left = `${tooltipLeft + scrollOffset.x}px`
544
+
545
+ return true
546
+ }
547
+
548
+ function drawArrow(anchorElementRect: DOMRect, currentTooltipPosition: TooltipPosition, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
549
+ // Create Arrow element
550
+ const arrowElement = document.createElement('div')
551
+
552
+ // Calculate Arrow element size, positions and style/angle classes
553
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
554
+ const arrowHalfLengthOfLongSide = Math.sin(45 * (180 / Math.PI)) * tooltipConfig.arrowSize
555
+
556
+ // Adjusts arrow position by `x` pixels to handle browsers sometimes not rendering border in it's full width, e.g., 4.8px instead of 5px
557
+ const arrowPositionAdjuster = 1;
558
+
559
+ // Arrow top/left 0 is Tooltip top/left 0
560
+ let arrowTop = 0
561
+ let arrowLeft = 0
562
+
563
+ let arrowClassForCorrectAngle = ''
564
+
565
+ switch (currentTooltipPosition) {
566
+ case "left":
567
+ arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-r-transparent'
568
+ arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
569
+ arrowLeft = tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
570
+ break;
571
+ case "top":
572
+ arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-b-transparent'
573
+ arrowTop = tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
574
+ arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
575
+ break;
576
+ case "right":
577
+ arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-l-transparent'
578
+ arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
579
+ arrowLeft = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
580
+ break;
581
+ case "bottom":
582
+ arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-t-transparent'
583
+ arrowTop = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
584
+ arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
585
+ break;
586
+ }
587
+
588
+ if (currentTooltipPosition === 'left' || currentTooltipPosition === 'right') {
589
+ if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)) {
590
+ arrowTop = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)
591
+ }
592
+ } else {
593
+ if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)) {
594
+ arrowLeft = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)
595
+ }
596
+ }
597
+
598
+ // Set Arrow element id, styling/angle
599
+ const adjustedArrowClasses = arrowElementClass + ' ' + defaultArrowClasses + ' ' + arrowClassForCorrectAngle + ' ' + tooltipConfig.arrowClasses
600
+
601
+ arrowElement.classList.add(...adjustedArrowClasses.trim().split(' '))
602
+
603
+ // Set Arrow element size and position
604
+ arrowElement.style.top = `${arrowTop}px`
605
+ arrowElement.style.left = `${arrowLeft}px`
606
+ arrowElement.style.borderWidth = `${tooltipConfig.arrowSize}px`
607
+
608
+ // Mount Arrow element
609
+ tooltipElement.appendChild(arrowElement)
610
+ }
611
+
612
+ function isArrowPositionWithinLimits(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
613
+ switch (currentTooltipPosition) {
614
+ case "left":
615
+ case "right":
616
+ return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
617
+ && arrowPosition < tooltipElementRect.height + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
618
+ case "top":
619
+ case "bottom":
620
+ return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
621
+ && arrowPosition < tooltipElementRect.width + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
622
+ }
623
+ }
624
+
625
+ function getArrowPositionMinLimit(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
626
+ switch (currentTooltipPosition) {
627
+ case "left":
628
+ case "right":
629
+ if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
630
+ // Arrow too close to viewport top
631
+ return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
632
+ } else {
633
+ // Arrow too close to viewport bottom
634
+ return tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
635
+ }
636
+ case "top":
637
+ case "bottom":
638
+ if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
639
+ // Arrow too close to viewport left
640
+ return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
641
+ } else {
642
+ // Arrow too close to viewport right
643
+ return tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
644
+ }
645
+ }
646
+ }
647
+
648
+ function hideTooltip(uuid: string) {
649
+ const tooltipElement = tooltips[uuid]?.tooltipElement
650
+
651
+ resetResizeReferences(uuid)
652
+ removeHideOnScrollListeners()
653
+
654
+ // Remove Arrow element from Tooltip, because it needs to be rebuilt every time Tooltip is showed again
655
+ tooltipElement.querySelector(`.${arrowElementClass}`)?.remove()
656
+
657
+ // Reset position so that old position does not effect new position (when zooming old position could be off screen)
658
+ tooltipElement.style.left = '0'
659
+ tooltipElement.style.top = '0'
660
+
661
+ tooltipElement.remove()
662
+ }
663
+
664
+ function destroyTooltip(tooltip: ReturnType<typeof initTooltip>) {
665
+ const uuid = tooltip.tooltipElement.dataset.uuid
666
+
667
+ if (uuid) {
668
+ removeRepositionOnResizeHandler(uuid)
669
+ hideTooltip(uuid)
670
+ delete tooltips[uuid]
671
+ }
672
+
673
+ for (const controller of Object.values(tooltip.mouseEnterEventControllers)) {
674
+ controller.abort()
675
+ }
676
+ }
677
+
678
+ export default ZeroTooltip