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