zero-tooltip 1.0.7 → 1.0.9

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