zero-tooltip 1.2.1 → 1.3.0

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,521 +1,577 @@
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
-
41
- const tooltips: {[key: string]: ReturnType<typeof initTooltip>} = {}
42
-
43
- const ZeroTooltip = (globalConfig?: TooltipConfig): Directive => {
44
- return {
45
- created: (targetElement: HTMLElement, binding, vnode) => {
46
- const uuid = uuidv4()
47
- vnode.el.$_tooltip = { uuid: uuid }
48
-
49
- buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
50
-
51
- if (typeof(binding.value) !== 'string' && isReactive(binding.value)) {
52
- watch(binding.value, (newBindingValue) => {
53
- if (tooltips[uuid]) {
54
- destroyTooltip(tooltips[uuid])
55
- }
56
-
57
- buildTooltip(newBindingValue, globalConfig, binding.arg, targetElement, uuid)
58
- })
59
- }
60
- },
61
-
62
- updated: (targetElement: HTMLElement, binding, vnode) => {
63
- const uuid = vnode.el.$_tooltip.uuid
64
-
65
- if (tooltips[uuid]) {
66
- destroyTooltip(tooltips[uuid])
67
- }
68
-
69
- buildTooltip(binding.value, globalConfig, binding.arg, targetElement, uuid)
70
- },
71
-
72
- beforeUnmount: (_, __, vnode) => {
73
- const uuid = vnode.el.$_tooltip.uuid
74
-
75
- if (tooltips[uuid]) {
76
- destroyTooltip(tooltips[uuid])
77
- }
78
- }
79
- }
80
- }
81
-
82
- function buildTooltip(bindingValue: any, globalConfig: TooltipConfig | undefined, bindingArgument: string | undefined, targetElement: HTMLElement, uuid: string) {
83
- let tooltipConfig = getTooltipConfig(bindingValue as string | TooltipLocalConfig, globalConfig, bindingArgument as TooltipPosition)
84
- const tooltip = initTooltip(targetElement, tooltipConfig, uuid)
85
-
86
- tooltips[uuid] = tooltip
87
-
88
- if (targetElement.matches(':hover')) {
89
- targetElement.dispatchEvent(new Event('mouseenter'))
90
- }
91
- }
92
-
93
- function getTooltipConfig(localConfig: string | TooltipLocalConfig, globalConfig?: TooltipConfig, position?: TooltipPosition) {
94
- // Tooltip config
95
- let appendTo = globalConfig?.appendTo ?? defaultAppendTo
96
- let tooltipText = getTooltipText(localConfig)
97
- let tooltipPosition = position ?? globalConfig?.defaultPosition ?? defaultTooltipPosition
98
- let tooltipPositions: TooltipPositions = {
99
- left: globalConfig?.positions?.left ?? defaultTooltipPositions.left,
100
- top: globalConfig?.positions?.top ?? defaultTooltipPositions.top,
101
- right: globalConfig?.positions?.right ?? defaultTooltipPositions.right,
102
- bottom: globalConfig?.positions?.bottom ?? defaultTooltipPositions.bottom,
103
- }
104
- let tooltipOffsetFromSource = globalConfig?.offsetFromSource ?? defaultTooltipOffsetFromSource
105
- let tooltipOffsetFromViewport = globalConfig?.offsetFromViewport ?? defaultTooltipOffsetFromViewport
106
- let tooltipMinWidth = globalConfig?.minWidth ?? defaultTooltipMinWidth
107
- let tooltipMaxWidth = globalConfig?.maxWidth ?? defaultTooltipMaxWidth
108
- let tooltipBorderWidth = globalConfig?.tooltipBorderWidth ?? defaultTooltipBorderWidth
109
- let tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + (globalConfig?.tooltipClasses ?? '')
110
- let textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + (globalConfig?.textClasses ?? '')
111
- let arrowSize = globalConfig?.arrowSize ?? defaultArrowSize
112
- let arrowClasses = globalConfig?.arrowClasses ?? ''
113
- let arrowMinOffsetFromTooltipCorner = globalConfig?.arrowMinOffsetFromTooltipCorner ?? defaultMinArrowOffsetFromTooltipCorner
114
- let zIndex = globalConfig?.zIndex ?? defaultZIndex
115
- let shouldShow = defaultShouldShow
116
-
117
- // 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)
118
- if (typeof(localConfig) !== 'string') {
119
- if (localConfig.appendTo !== undefined) appendTo = localConfig.appendTo
120
- if (position === undefined && localConfig.defaultPosition !== undefined) tooltipPosition = localConfig.defaultPosition
121
-
122
- if (localConfig.positions?.left !== undefined) tooltipPositions.left = localConfig.positions.left
123
- if (localConfig.positions?.top !== undefined) tooltipPositions.top = localConfig.positions.top
124
- if (localConfig.positions?.right !== undefined) tooltipPositions.right = localConfig.positions.right
125
- if (localConfig.positions?.bottom !== undefined) tooltipPositions.bottom = localConfig.positions.bottom
126
-
127
- if (localConfig.offsetFromSource !== undefined) tooltipOffsetFromSource = localConfig.offsetFromSource
128
- if (localConfig.offsetFromViewport !== undefined) tooltipOffsetFromViewport = localConfig.offsetFromViewport
129
- if (localConfig.minWidth !== undefined) tooltipMinWidth = localConfig.minWidth
130
- if (localConfig.maxWidth !== undefined) tooltipMaxWidth = localConfig.maxWidth
131
- if (localConfig.tooltipBorderWidth !== undefined) tooltipBorderWidth = localConfig.tooltipBorderWidth
132
- if (localConfig.tooltipClasses !== undefined) tooltipClasses = tooltipElementClass + ' ' + defaultTooltipClasses + ' ' + localConfig.tooltipClasses
133
- if (localConfig.textClasses !== undefined) textClasses = textElementClass + ' ' + defaultTextClasses + ' ' + localConfig.textClasses
134
- if (localConfig.arrowSize !== undefined) arrowSize = localConfig.arrowSize
135
- if (localConfig.arrowClasses !== undefined) arrowClasses = localConfig.arrowClasses
136
- if (localConfig.arrowMinOffsetFromTooltipCorner !== undefined) arrowMinOffsetFromTooltipCorner = localConfig.arrowMinOffsetFromTooltipCorner
137
- if (localConfig.zIndex !== undefined) zIndex = localConfig.zIndex
138
- if (localConfig.show !== undefined) shouldShow = localConfig.show
139
- }
140
-
141
- return {
142
- appendTo,
143
- tooltipText,
144
- tooltipPosition,
145
- tooltipPositions,
146
- tooltipOffsetFromSource,
147
- tooltipOffsetFromViewport,
148
- tooltipMinWidth,
149
- tooltipMaxWidth,
150
- tooltipBorderWidth,
151
- tooltipClasses,
152
- textClasses,
153
- arrowSize,
154
- arrowClasses,
155
- arrowMinOffsetFromTooltipCorner,
156
- zIndex,
157
- shouldShow
158
- }
159
- }
160
-
161
- function getTooltipText(localConfig: string | TooltipLocalConfig) {
162
- const tooltipText = typeof(localConfig) === 'string' ? localConfig : localConfig.content
163
-
164
- if (!tooltipText) {
165
- throw new Error("Please enter valid tooltip value");
166
- }
167
-
168
- return tooltipText
169
- }
170
-
171
- function initTooltip(targetElement: HTMLElement, tooltipConfig: ReturnType<typeof getTooltipConfig>, uuid: string) {
172
- let anchorElement = targetElement
173
-
174
- let tooltipTextElement = createTextElement(tooltipConfig.textClasses, tooltipConfig.tooltipText)
175
- let tooltipElement = createTooltipElement(tooltipConfig.tooltipClasses, tooltipConfig.tooltipBorderWidth)
176
- tooltipElement.append(tooltipTextElement)
177
- tooltipElement.dataset.uuid = uuid
178
-
179
- const mouseEnterEventController = new AbortController()
180
- const mouseLeaveEventController = new AbortController()
181
-
182
- anchorElement.addEventListener('mouseenter', () => onMouseEnter(anchorElement, tooltipConfig, tooltipElement, uuid), { signal: mouseEnterEventController.signal})
183
- anchorElement.addEventListener('mouseleave', () => onMouseLeave(uuid), { signal: mouseLeaveEventController.signal})
184
-
185
- return {
186
- anchorElement,
187
- tooltipConfig,
188
- tooltipElement,
189
- mouseEnterEventController,
190
- mouseLeaveEventController
191
- }
192
- }
193
-
194
- function createTextElement(textClasses: string, tooltipText: string) {
195
- let tooltipTextElement = document.createElement('p')
196
- tooltipTextElement.classList.add(...textClasses.trim().split(' '))
197
- tooltipTextElement.innerHTML = tooltipText
198
-
199
- return tooltipTextElement
200
- }
201
-
202
- function createTooltipElement(tooltipClasses: string, tooltipBorderWidth: number) {
203
- let tooltipElement = document.createElement('div')
204
- tooltipElement.classList.add(...tooltipClasses.trim().split(' '))
205
- tooltipElement.style.borderWidth = `${tooltipBorderWidth}px`
206
-
207
- return tooltipElement
208
- }
209
-
210
- function onMouseEnter(
211
- anchorElement: HTMLElement,
212
- tooltipConfig: ReturnType<typeof getTooltipConfig>,
213
- tooltipElement: HTMLDivElement,
214
- uuid: string
215
- ) {
216
- if (!tooltipConfig.shouldShow) return
217
-
218
- const anchorElementRect = anchorElement.getBoundingClientRect()
219
-
220
- // Mount Tooltip element to target element (default is `body`)
221
- const appendToTarget = document.querySelector(tooltipConfig.appendTo)
222
- appendToTarget?.appendChild(tooltipElement)
223
-
224
- // Find suitable Tooltip position
225
- let hasNeededDisplaySpace = false
226
- let currentTooltipPosition = tooltipConfig.tooltipPosition
227
- for (let i = 0; i < 4; i++) {
228
- currentTooltipPosition = tooltipConfig.tooltipPositions[tooltipConfig.tooltipPosition][i]
229
-
230
- if (currentTooltipPosition === 'left') {
231
- hasNeededDisplaySpace = tryMountTooltipOnLeft(anchorElementRect, tooltipConfig, tooltipElement)
232
- } else if (currentTooltipPosition === 'top') {
233
- hasNeededDisplaySpace = tryMountTooltipOnTop(anchorElementRect, tooltipConfig, tooltipElement)
234
- } else if (currentTooltipPosition === 'right') {
235
- hasNeededDisplaySpace = tryMountTooltipOnRight(anchorElementRect, tooltipConfig, tooltipElement)
236
- } else if (currentTooltipPosition === 'bottom') {
237
- hasNeededDisplaySpace = tryMountTooltipOnBottom(anchorElementRect, tooltipConfig, tooltipElement)
238
- }
239
-
240
- if (hasNeededDisplaySpace) break
241
- }
242
-
243
- if (hasNeededDisplaySpace) {
244
- drawArrow(anchorElementRect, currentTooltipPosition, tooltipConfig, tooltipElement)
245
-
246
- tooltipElement.style.opacity = '1'
247
- tooltipElement.style.zIndex = typeof(tooltipConfig.zIndex) === 'string' ? tooltipConfig.zIndex : tooltipConfig.zIndex.toString();
248
-
249
- handleHideOnScroll(anchorElement, () => hideTooltip(uuid))
250
- handleHideOnResize(anchorElement, () => hideTooltip(uuid))
251
- }
252
- }
253
-
254
- function onMouseLeave(uuid: string) {
255
- hideTooltip(uuid)
256
- }
257
-
258
- function tryMountTooltipOnLeft(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
259
- // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
260
- const tooltipAvailableMaxWidth = Math.min(anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
261
- const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
262
- const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
263
-
264
- if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
265
-
266
- // Set Tooltip maxWidth
267
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
268
-
269
- // Calculate Tooltip position
270
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
271
- let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
272
-
273
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
274
- tooltipTop = tooltipConfig.tooltipOffsetFromViewport
275
- } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
276
- tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
277
- }
278
-
279
- const tooltipLeft = anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.width
280
-
281
- // Check if anchor element is directly on right of Tooltip
282
- if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
283
- || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
284
-
285
- // Set Tooltip position
286
- tooltipElement.style.top = `${tooltipTop}px`
287
- tooltipElement.style.left = `${tooltipLeft}px`
288
-
289
- return true
290
- }
291
-
292
- function tryMountTooltipOnRight(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
293
- // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
294
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource) - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
295
- const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
296
- const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
297
-
298
- if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
299
-
300
- // Set tooltip maxWidth
301
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
302
-
303
- // Calculate Tooltip position
304
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
305
-
306
- let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
307
-
308
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
309
- tooltipTop = tooltipConfig.tooltipOffsetFromViewport
310
- } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
311
- tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
312
- }
313
-
314
- const tooltipLeft = anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource
315
-
316
- // Check if anchor element is directly on left of Tooltip
317
- if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
318
- || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
319
-
320
- // Set Tooltip position
321
- tooltipElement.style.top = `${tooltipTop}px`
322
- tooltipElement.style.left = `${tooltipLeft}px`
323
-
324
- return true
325
- }
326
-
327
- function tryMountTooltipOnTop(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
328
- // Calculate and set Tooltip width
329
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
330
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
331
-
332
- // Calculate Tooltip top position
333
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
334
- let tooltipTop = anchorElementRect.top - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.height
335
-
336
- // Check if Tooltip has enough available on top
337
- if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) return false
338
-
339
- // Calculate Tooltip left position
340
- let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
341
-
342
- if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
343
- tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
344
- } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
345
- tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
346
- }
347
-
348
- // Check if anchor element is directly on below of Tooltip
349
- if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
350
- || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
351
-
352
- // Set Tooltip position
353
- tooltipElement.style.top = `${tooltipTop}px`
354
- tooltipElement.style.left = `${tooltipLeft}px`
355
-
356
- return true
357
- }
358
-
359
- function tryMountTooltipOnBottom(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
360
- // Calculate and set Tooltip width
361
- const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
362
- tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
363
-
364
- // Calculate Tooltip top position
365
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
366
- let tooltipTop = anchorElementRect.bottom + tooltipConfig.tooltipOffsetFromSource
367
-
368
- // Check if Tooltip has enough available on bottom
369
- if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) return false
370
-
371
- // Calculate Tooltip left position
372
- let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
373
-
374
- if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
375
- tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
376
- } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
377
- tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
378
- }
379
-
380
- // Check if anchor element is directly on top of Tooltip
381
- if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
382
- || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
383
-
384
- // Set Tooltip position
385
- tooltipElement.style.top = `${tooltipTop}px`
386
- tooltipElement.style.left = `${tooltipLeft}px`
387
-
388
- return true
389
- }
390
-
391
- function drawArrow(anchorElementRect: DOMRect, currentTooltipPosition: TooltipPosition, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
392
- // Create Arrow element
393
- const arrowElement = document.createElement('div')
394
-
395
- // Calculate Arrow element size, positions and style/angle classes
396
- const tooltipElementRect = tooltipElement.getBoundingClientRect()
397
- const arrowHalfLengthOfLongSide = Math.sin(45 * (180 / Math.PI)) * tooltipConfig.arrowSize
398
-
399
- // 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
400
- const arrowPositionAdjuster = 1;
401
-
402
- // Arrow top/left 0 is Tooltip top/left 0
403
- let arrowTop = 0
404
- let arrowLeft = 0
405
-
406
- let arrowClassForCorrectAngle = ''
407
-
408
- switch (currentTooltipPosition) {
409
- case "left":
410
- arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-r-transparent'
411
- arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
412
- arrowLeft = tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
413
- break;
414
- case "top":
415
- arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-b-transparent'
416
- arrowTop = tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
417
- arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
418
- break;
419
- case "right":
420
- arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-l-transparent'
421
- arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
422
- arrowLeft = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
423
- break;
424
- case "bottom":
425
- arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-t-transparent'
426
- arrowTop = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
427
- arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
428
- break;
429
- }
430
-
431
- if (currentTooltipPosition === 'left' || currentTooltipPosition === 'right') {
432
- if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)) {
433
- arrowTop = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)
434
- }
435
- } else {
436
- if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)) {
437
- arrowLeft = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)
438
- }
439
- }
440
-
441
- // Set Arrow element id, styling/angle
442
- const adjustedArrowClasses = arrowElementClass + ' ' + defaultArrowClasses + ' ' + arrowClassForCorrectAngle + ' ' + tooltipConfig.arrowClasses
443
-
444
- arrowElement.classList.add(...adjustedArrowClasses.trim().split(' '))
445
-
446
- // Set Arrow element size and position
447
- arrowElement.style.top = `${arrowTop}px`
448
- arrowElement.style.left = `${arrowLeft}px`
449
- arrowElement.style.borderWidth = `${tooltipConfig.arrowSize}px`
450
-
451
- // Mount Arrow element
452
- document.querySelector(`.${tooltipElementClass}`)?.appendChild(arrowElement)
453
- }
454
-
455
- function isArrowPositionWithinLimits(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
456
- switch (currentTooltipPosition) {
457
- case "left":
458
- case "right":
459
- return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
460
- && arrowPosition < tooltipElementRect.height + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
461
- case "top":
462
- case "bottom":
463
- return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
464
- && arrowPosition < tooltipElementRect.width + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
465
- }
466
- }
467
-
468
- function getArrowPositionMinLimit(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
469
- switch (currentTooltipPosition) {
470
- case "left":
471
- case "right":
472
- if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
473
- // Arrow too close to viewport top
474
- return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
475
- } else {
476
- // Arrow too close to viewport bottom
477
- return tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
478
- }
479
- case "top":
480
- case "bottom":
481
- if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
482
- // Arrow too close to viewport left
483
- return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
484
- } else {
485
- // Arrow too close to viewport right
486
- return tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
487
- }
488
- }
489
- }
490
-
491
- function hideTooltip(uuid: string) {
492
- const shownTooltipElement = document.querySelector(`.${tooltipElementClass}`)
493
- const currentTooltipElement = tooltips[uuid]?.tooltipElement
494
-
495
- if (currentTooltipElement && shownTooltipElement && shownTooltipElement instanceof HTMLElement && shownTooltipElement === currentTooltipElement) {
496
- resetResizeReferences()
497
-
498
- // Remove Arrow element from Tooltip, because it needs to be rebuilt every time Tooltip is showed again
499
- shownTooltipElement.querySelector(`.${arrowElementClass}`)?.remove()
500
-
501
- // Reset position so that old position does not effect new position (when zooming old position could be off screen)
502
- shownTooltipElement.style.left = '0'
503
- shownTooltipElement.style.top = '0'
504
-
505
- shownTooltipElement.remove()
506
- }
507
- }
508
-
509
- function destroyTooltip(tooltip: ReturnType<typeof initTooltip>) {
510
- const uuid = tooltip.tooltipElement.dataset.uuid
511
-
512
- if (uuid) {
513
- hideTooltip(uuid)
514
- delete tooltips[uuid]
515
- }
516
-
517
- tooltip.mouseEnterEventController.abort()
518
- tooltip.mouseLeaveEventController.abort()
519
- }
520
-
521
- 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
+
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: Date.now(),
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, tooltipConfig.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(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 _hideDelay = options?.isTooltip ? 0 : tooltipConfig.hideDelay
300
+ const currentInstanceId = Date.now()
301
+ tooltips[uuid].mouseEventState.currentInstanceId = currentInstanceId
302
+ tooltips[uuid].mouseEventState.isHoveringOverAnchorElement = false
303
+
304
+ if (_hideDelay > 0) {
305
+ await new Promise(resolve => setTimeout(resolve, tooltipConfig.hideDelay))
306
+
307
+ if (tooltips[uuid].mouseEventState.isHoveringOverAnchorElement || tooltips[uuid].mouseEventState.currentInstanceId !== currentInstanceId) return
308
+ }
309
+
310
+ hideTooltip(uuid)
311
+ }
312
+
313
+ function tryMountTooltipOnLeft(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
314
+ // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
315
+ const tooltipAvailableMaxWidth = Math.min(anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
316
+ const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
317
+ const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
318
+
319
+ if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
320
+
321
+ // Set Tooltip maxWidth
322
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
323
+
324
+ // Calculate Tooltip position
325
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
326
+ let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
327
+
328
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
329
+ tooltipTop = tooltipConfig.tooltipOffsetFromViewport
330
+ } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
331
+ tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
332
+ }
333
+
334
+ const tooltipLeft = anchorElementRect.left - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.width
335
+
336
+ // Check if anchor element is directly on right of Tooltip
337
+ if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
338
+ || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
339
+
340
+ // Set Tooltip position
341
+ tooltipElement.style.top = `${tooltipTop}px`
342
+ tooltipElement.style.left = `${tooltipLeft}px`
343
+
344
+ return true
345
+ }
346
+
347
+ function tryMountTooltipOnRight(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
348
+ // Check if Tooltip has enough available horizontal space, top and bottom offset from viewport
349
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource) - tooltipConfig.tooltipOffsetFromViewport, tooltipConfig.tooltipMaxWidth)
350
+ const isAnchorElementTopLowerThanOffsetFromViewport = anchorElementRect.top >= tooltipConfig.tooltipOffsetFromViewport
351
+ const isAnchorElementBottomHigherThanOffsetFromViewport = (window.innerHeight - anchorElementRect.bottom) >= tooltipConfig.tooltipOffsetFromViewport
352
+
353
+ if (tooltipAvailableMaxWidth < tooltipConfig.tooltipMinWidth || !isAnchorElementTopLowerThanOffsetFromViewport || !isAnchorElementBottomHigherThanOffsetFromViewport) return false
354
+
355
+ // Set tooltip maxWidth
356
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
357
+
358
+ // Calculate Tooltip position
359
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
360
+
361
+ let tooltipTop = anchorElementRect.top + (anchorElementRect.height / 2) - (tooltipElementRect.height / 2)
362
+
363
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) {
364
+ tooltipTop = tooltipConfig.tooltipOffsetFromViewport
365
+ } else if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) {
366
+ tooltipTop = window.innerHeight - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.height
367
+ }
368
+
369
+ const tooltipLeft = anchorElementRect.right + tooltipConfig.tooltipOffsetFromSource
370
+
371
+ // Check if anchor element is directly on left of Tooltip
372
+ if (anchorElementRect.bottom < tooltipTop + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
373
+ || anchorElementRect.top > tooltipTop + tooltipElementRect.height - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
374
+
375
+ // Set Tooltip position
376
+ tooltipElement.style.top = `${tooltipTop}px`
377
+ tooltipElement.style.left = `${tooltipLeft}px`
378
+
379
+ return true
380
+ }
381
+
382
+ function tryMountTooltipOnTop(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
383
+ // Calculate and set Tooltip width
384
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
385
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
386
+
387
+ // Calculate Tooltip top position
388
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
389
+ let tooltipTop = anchorElementRect.top - tooltipConfig.tooltipOffsetFromSource - tooltipElementRect.height
390
+
391
+ // Check if Tooltip has enough available on top
392
+ if (tooltipTop < tooltipConfig.tooltipOffsetFromViewport) return false
393
+
394
+ // Calculate Tooltip left position
395
+ let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
396
+
397
+ if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
398
+ tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
399
+ } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
400
+ tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
401
+ }
402
+
403
+ // Check if anchor element is directly on below of Tooltip
404
+ if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
405
+ || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
406
+
407
+ // Set Tooltip position
408
+ tooltipElement.style.top = `${tooltipTop}px`
409
+ tooltipElement.style.left = `${tooltipLeft}px`
410
+
411
+ return true
412
+ }
413
+
414
+ function tryMountTooltipOnBottom(anchorElementRect: DOMRect, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
415
+ // Calculate and set Tooltip width
416
+ const tooltipAvailableMaxWidth = Math.min(window.innerWidth - (tooltipConfig.tooltipOffsetFromViewport * 2), tooltipConfig.tooltipMaxWidth)
417
+ tooltipElement.style.maxWidth = `${tooltipAvailableMaxWidth}px`
418
+
419
+ // Calculate Tooltip top position
420
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
421
+ let tooltipTop = anchorElementRect.bottom + tooltipConfig.tooltipOffsetFromSource
422
+
423
+ // Check if Tooltip has enough available on bottom
424
+ if (tooltipTop + tooltipElementRect.height > window.innerHeight - tooltipConfig.tooltipOffsetFromViewport) return false
425
+
426
+ // Calculate Tooltip left position
427
+ let tooltipLeft = anchorElementRect.left + (anchorElementRect.width / 2) - (tooltipElementRect.width / 2)
428
+
429
+ if (tooltipLeft < tooltipConfig.tooltipOffsetFromViewport) {
430
+ tooltipLeft = tooltipConfig.tooltipOffsetFromViewport
431
+ } else if (tooltipLeft + tooltipElementRect.width > window.innerWidth - tooltipConfig.tooltipOffsetFromViewport) {
432
+ tooltipLeft = window.innerWidth - tooltipConfig.tooltipOffsetFromViewport - tooltipElementRect.width
433
+ }
434
+
435
+ // Check if anchor element is directly on top of Tooltip
436
+ if (anchorElementRect.left > tooltipLeft + tooltipElementRect.width - tooltipConfig.arrowMinOffsetFromTooltipCorner * 2
437
+ || anchorElementRect.right < tooltipLeft + tooltipConfig.arrowMinOffsetFromTooltipCorner * 2) return false
438
+
439
+ // Set Tooltip position
440
+ tooltipElement.style.top = `${tooltipTop}px`
441
+ tooltipElement.style.left = `${tooltipLeft}px`
442
+
443
+ return true
444
+ }
445
+
446
+ function drawArrow(anchorElementRect: DOMRect, currentTooltipPosition: TooltipPosition, tooltipConfig: ReturnType<typeof getTooltipConfig>, tooltipElement: HTMLDivElement) {
447
+ // Create Arrow element
448
+ const arrowElement = document.createElement('div')
449
+
450
+ // Calculate Arrow element size, positions and style/angle classes
451
+ const tooltipElementRect = tooltipElement.getBoundingClientRect()
452
+ const arrowHalfLengthOfLongSide = Math.sin(45 * (180 / Math.PI)) * tooltipConfig.arrowSize
453
+
454
+ // 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
455
+ const arrowPositionAdjuster = 1;
456
+
457
+ // Arrow top/left 0 is Tooltip top/left 0
458
+ let arrowTop = 0
459
+ let arrowLeft = 0
460
+
461
+ let arrowClassForCorrectAngle = ''
462
+
463
+ switch (currentTooltipPosition) {
464
+ case "left":
465
+ arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-r-transparent'
466
+ arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
467
+ arrowLeft = tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
468
+ break;
469
+ case "top":
470
+ arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-b-transparent'
471
+ arrowTop = tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - arrowPositionAdjuster
472
+ arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
473
+ break;
474
+ case "right":
475
+ arrowClassForCorrectAngle = '!zt-border-y-transparent !zt-border-l-transparent'
476
+ arrowTop = anchorElementRect.top - tooltipElementRect.top + (anchorElementRect.height / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
477
+ arrowLeft = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
478
+ break;
479
+ case "bottom":
480
+ arrowClassForCorrectAngle = '!zt-border-x-transparent !zt-border-t-transparent'
481
+ arrowTop = (-tooltipConfig.arrowSize * 2) - tooltipConfig.tooltipBorderWidth + arrowPositionAdjuster
482
+ arrowLeft = anchorElementRect.left - tooltipElementRect.left + (anchorElementRect.width / 2) - arrowHalfLengthOfLongSide - tooltipConfig.tooltipBorderWidth
483
+ break;
484
+ }
485
+
486
+ if (currentTooltipPosition === 'left' || currentTooltipPosition === 'right') {
487
+ if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)) {
488
+ arrowTop = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowTop, tooltipConfig)
489
+ }
490
+ } else {
491
+ if (!isArrowPositionWithinLimits(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)) {
492
+ arrowLeft = getArrowPositionMinLimit(currentTooltipPosition, tooltipElementRect, arrowLeft, tooltipConfig)
493
+ }
494
+ }
495
+
496
+ // Set Arrow element id, styling/angle
497
+ const adjustedArrowClasses = arrowElementClass + ' ' + defaultArrowClasses + ' ' + arrowClassForCorrectAngle + ' ' + tooltipConfig.arrowClasses
498
+
499
+ arrowElement.classList.add(...adjustedArrowClasses.trim().split(' '))
500
+
501
+ // Set Arrow element size and position
502
+ arrowElement.style.top = `${arrowTop}px`
503
+ arrowElement.style.left = `${arrowLeft}px`
504
+ arrowElement.style.borderWidth = `${tooltipConfig.arrowSize}px`
505
+
506
+ // Mount Arrow element
507
+ document.querySelector(`.${tooltipElementClass}`)?.appendChild(arrowElement)
508
+ }
509
+
510
+ function isArrowPositionWithinLimits(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
511
+ switch (currentTooltipPosition) {
512
+ case "left":
513
+ case "right":
514
+ return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
515
+ && arrowPosition < tooltipElementRect.height + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
516
+ case "top":
517
+ case "bottom":
518
+ return arrowPosition > tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
519
+ && arrowPosition < tooltipElementRect.width + tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
520
+ }
521
+ }
522
+
523
+ function getArrowPositionMinLimit(currentTooltipPosition: TooltipPosition, tooltipElementRect: DOMRect, arrowPosition: number, tooltipConfig: ReturnType<typeof getTooltipConfig>) {
524
+ switch (currentTooltipPosition) {
525
+ case "left":
526
+ case "right":
527
+ if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
528
+ // Arrow too close to viewport top
529
+ return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
530
+ } else {
531
+ // Arrow too close to viewport bottom
532
+ return tooltipElementRect.height - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
533
+ }
534
+ case "top":
535
+ case "bottom":
536
+ if (arrowPosition < tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth) {
537
+ // Arrow too close to viewport left
538
+ return tooltipConfig.arrowMinOffsetFromTooltipCorner - tooltipConfig.tooltipBorderWidth
539
+ } else {
540
+ // Arrow too close to viewport right
541
+ return tooltipElementRect.width - tooltipConfig.tooltipBorderWidth - tooltipConfig.arrowMinOffsetFromTooltipCorner - (tooltipConfig.arrowSize * 2)
542
+ }
543
+ }
544
+ }
545
+
546
+ function hideTooltip(uuid: string) {
547
+ const shownTooltipElement = document.querySelector(`.${tooltipElementClass}`)
548
+ const currentTooltipElement = tooltips[uuid]?.tooltipElement
549
+
550
+ if (currentTooltipElement && shownTooltipElement && shownTooltipElement instanceof HTMLElement && shownTooltipElement === currentTooltipElement) {
551
+ resetResizeReferences()
552
+
553
+ // Remove Arrow element from Tooltip, because it needs to be rebuilt every time Tooltip is showed again
554
+ shownTooltipElement.querySelector(`.${arrowElementClass}`)?.remove()
555
+
556
+ // Reset position so that old position does not effect new position (when zooming old position could be off screen)
557
+ shownTooltipElement.style.left = '0'
558
+ shownTooltipElement.style.top = '0'
559
+
560
+ shownTooltipElement.remove()
561
+ }
562
+ }
563
+
564
+ function destroyTooltip(tooltip: ReturnType<typeof initTooltip>) {
565
+ const uuid = tooltip.tooltipElement.dataset.uuid
566
+
567
+ if (uuid) {
568
+ hideTooltip(uuid)
569
+ delete tooltips[uuid]
570
+ }
571
+
572
+ for (const controller of Object.values(tooltip.mouseEnterEventControllers)) {
573
+ controller.abort()
574
+ }
575
+ }
576
+
577
+ export default ZeroTooltip