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