adata-ui 2.0.80 → 2.0.82
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.
|
@@ -1,74 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
import {twJoin, twMerge} from "tailwind-merge"
|
|
4
|
-
|
|
5
|
-
const uiConfig = {
|
|
6
|
-
wrapper: 'relative',
|
|
7
|
-
container: {
|
|
8
|
-
base: 'group absolute',
|
|
9
|
-
zIndex: 'z-[999]',
|
|
10
|
-
placement: {
|
|
11
|
-
top: '-top-1 left-1/2',
|
|
12
|
-
bottom: '-bottom-1 left-1/2',
|
|
13
|
-
left: '-left-1 top-1/2',
|
|
14
|
-
right: '-right-1 top-1/2'
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
width: 'max-w-xs',
|
|
18
|
-
background: 'bg-deepblue-900 dark:bg-gray-50',
|
|
19
|
-
backgroundError: 'bg-[#FAD9D7] dark:bg-[#4F393A]',
|
|
20
|
-
color: 'text-white dark:text-deepblue-900',
|
|
21
|
-
colorError: 'text-[#E74135] dark:text-[#F47E75]',
|
|
22
|
-
rounded: 'rounded-md',
|
|
23
|
-
base: 'text-xs font-normal absolute',
|
|
24
|
-
padding: {
|
|
25
|
-
sm: 'px-4 py-2',
|
|
26
|
-
md: 'px-4 py-4'
|
|
27
|
-
},
|
|
28
|
-
placement: {
|
|
29
|
-
top: {
|
|
30
|
-
start: 'bottom-[1px] left-[-13px]',
|
|
31
|
-
center: 'bottom-[1px] left-1/2 transform translate-x-[-50%]',
|
|
32
|
-
end: 'bottom-[1px] right-[-13px]'
|
|
33
|
-
},
|
|
34
|
-
bottom: {
|
|
35
|
-
start: 'top-[1px] left-[-13px]',
|
|
36
|
-
center: 'top-[1px] left-1/2 transform translate-x-[-50%]',
|
|
37
|
-
end: 'top-[1px] right-[-13px]'
|
|
38
|
-
},
|
|
39
|
-
left: {
|
|
40
|
-
start: 'right-[1px] top-[-13px]',
|
|
41
|
-
center: 'right-[1px] top-1/2 transform translate-y-[-50%]',
|
|
42
|
-
end: 'right-[1px] bottom-[-13px]'
|
|
43
|
-
},
|
|
44
|
-
right: {
|
|
45
|
-
start: 'left-[1px] top-[-13px]',
|
|
46
|
-
center: 'left-[1px] top-1/2 transform translate-y-[-50%]',
|
|
47
|
-
end: 'left-[1px] bottom-[-13px]'
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
transition: {
|
|
51
|
-
enterActiveClass: 'transition ease-out duration-200',
|
|
52
|
-
enterFromClass: 'opacity-0 translate-y-1',
|
|
53
|
-
enterToClass: 'opacity-100 translate-y-0',
|
|
54
|
-
leaveActiveClass: 'transition ease-in duration-150',
|
|
55
|
-
leaveFromClass: 'opacity-100 translate-y-0',
|
|
56
|
-
leaveToClass: 'opacity-0 translate-y-1'
|
|
57
|
-
},
|
|
58
|
-
popper: {
|
|
59
|
-
strategy: 'absolute',
|
|
60
|
-
base: 'invisible before:visible before:block before:absolute before:rotate-45 before:z-19 before:w-3 before:h-3',
|
|
61
|
-
rounded: 'before:rounded-sm',
|
|
62
|
-
background: 'before:bg-deepblue-900 dark:before:bg-gray-50',
|
|
63
|
-
backgroundError: 'before:bg-[#FAD9D7] dark:before:dark:bg-[#4F393A]',
|
|
64
|
-
placement: {
|
|
65
|
-
top: 'bottom-0 before:bottom-[-2px] before:left-[-6px]',
|
|
66
|
-
bottom: 'top-0 before:top-[-2px] before:left-[-6px]',
|
|
67
|
-
left: 'right-0 before:right-[-2px] before:top-[-6px]',
|
|
68
|
-
right: 'left-0 before:left-[-2px] before:top-[-6px]'
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
}
|
|
2
|
+
import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
72
3
|
|
|
73
4
|
interface Props {
|
|
74
5
|
text?: string
|
|
@@ -100,143 +31,207 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
100
31
|
errorParsing: false
|
|
101
32
|
})
|
|
102
33
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
34
|
+
/* --- refs --- */
|
|
35
|
+
const wrapper = ref<HTMLElement | null>(null) // root wrapper
|
|
36
|
+
const body = ref<HTMLElement | null>(null) // tooltip element
|
|
106
37
|
const open = ref(false)
|
|
107
|
-
let openTimeout:
|
|
108
|
-
let closeTimeout:
|
|
38
|
+
let openTimeout: ReturnType<typeof setTimeout> | null = null
|
|
39
|
+
let closeTimeout: ReturnType<typeof setTimeout> | null = null
|
|
40
|
+
|
|
41
|
+
// inline styles for tooltip positioning
|
|
42
|
+
const tooltipStyle = ref<Record<string, string>>({
|
|
43
|
+
left: '0px',
|
|
44
|
+
top: '0px',
|
|
45
|
+
transform: 'translateX(0) translateY(0)'
|
|
46
|
+
})
|
|
109
47
|
|
|
48
|
+
// visual placement (may be flipped if not enough space)
|
|
49
|
+
const visualPlacement = ref(props.placement)
|
|
110
50
|
const popperPosition = ref(props.popperPosition)
|
|
111
51
|
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
props.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
uiConfig.popper.strategy,
|
|
121
|
-
uiConfig.popper.base,
|
|
122
|
-
uiConfig.popper.rounded,
|
|
123
|
-
props.errorParsing ? uiConfig.popper.backgroundError : uiConfig.popper.background,
|
|
124
|
-
uiConfig.popper.placement[props.placement]
|
|
125
|
-
)))
|
|
126
|
-
const baseClasses = computed(() => twMerge(twJoin(
|
|
127
|
-
uiConfig.base,
|
|
128
|
-
props.errorParsing ? uiConfig.backgroundError : uiConfig.background,
|
|
129
|
-
props.errorParsing ? uiConfig.colorError : uiConfig.color,
|
|
130
|
-
uiConfig.rounded,
|
|
131
|
-
uiConfig.placement[props.placement][popperPosition.value],
|
|
132
|
-
uiConfig.padding[props.size],
|
|
133
|
-
props.truncate ? 'truncate' : 'max-w-[530px] w-max'
|
|
134
|
-
)))
|
|
135
|
-
const transitionClasses = uiConfig.transition
|
|
136
|
-
|
|
137
|
-
const containerStyles = computed(() => {
|
|
138
|
-
return {
|
|
139
|
-
zIndex: props.zIndex
|
|
140
|
-
}
|
|
52
|
+
/* --- helpers: compute classes (you can keep your uiConfig) --- */
|
|
53
|
+
const baseClass = computed(() => {
|
|
54
|
+
// keep your Tailwind classes if you like, but we position via inline styles
|
|
55
|
+
const bg = props.errorParsing ? 'bg-[#FAD9D7] dark:bg-[#4F393A]' : 'bg-deepblue-900 dark:bg-gray-50'
|
|
56
|
+
const color = props.errorParsing ? 'text-[#E74135] dark:text-[#F47E75]' : 'text-white dark:text-deepblue-900'
|
|
57
|
+
const pad = props.size === 'sm' ? 'px-4 py-2' : 'px-4 py-4'
|
|
58
|
+
const truncated = props.truncate ? 'truncate' : 'max-w-[530px] w-max'
|
|
59
|
+
return ['text-xs font-normal absolute', bg, color, 'rounded-md', pad, truncated].join(' ')
|
|
141
60
|
})
|
|
142
61
|
|
|
62
|
+
/* --- positioning logic --- */
|
|
63
|
+
function updatePosition(targetEl: HTMLElement | null) {
|
|
64
|
+
if (!targetEl || !body.value) return
|
|
65
|
+
|
|
66
|
+
const targetRect = targetEl.getBoundingClientRect()
|
|
67
|
+
const tooltipRect = body.value.getBoundingClientRect()
|
|
68
|
+
const vw = window.innerWidth
|
|
69
|
+
const vh = window.innerHeight
|
|
70
|
+
|
|
71
|
+
// default placement = requested prop
|
|
72
|
+
let place: Props['placement'] = props.placement
|
|
73
|
+
|
|
74
|
+
// decide flip if not enough space
|
|
75
|
+
if (place === 'top' && targetRect.top < tooltipRect.height + 8 && (vh - targetRect.bottom) >= tooltipRect.height + 8) {
|
|
76
|
+
place = 'bottom'
|
|
77
|
+
} else if (place === 'bottom' && (vh - targetRect.bottom) < tooltipRect.height + 8 && targetRect.top >= tooltipRect.height + 8) {
|
|
78
|
+
place = 'top'
|
|
79
|
+
} else if (place === 'left' && targetRect.left < tooltipRect.width + 8 && (vw - targetRect.right) >= tooltipRect.width + 8) {
|
|
80
|
+
place = 'right'
|
|
81
|
+
} else if (place === 'right' && (vw - targetRect.right) < tooltipRect.width + 8 && targetRect.left >= tooltipRect.width + 8) {
|
|
82
|
+
place = 'left'
|
|
83
|
+
}
|
|
143
84
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
85
|
+
visualPlacement.value = place
|
|
86
|
+
|
|
87
|
+
// compute left/top depending on placement and prefer 'start' unless end/center chosen by popperPosition
|
|
88
|
+
let left = 0
|
|
89
|
+
let top = 0
|
|
90
|
+
|
|
91
|
+
if (place === 'top' || place === 'bottom') {
|
|
92
|
+
// horizontal alignment depends on popperPosition
|
|
93
|
+
if (popperPosition.value === 'center') {
|
|
94
|
+
left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
|
|
95
|
+
} else if (popperPosition.value === 'end') {
|
|
96
|
+
left = targetRect.right - tooltipRect.width
|
|
97
|
+
} else {
|
|
98
|
+
// start
|
|
99
|
+
left = targetRect.left
|
|
100
|
+
}
|
|
147
101
|
|
|
148
|
-
|
|
102
|
+
// vertical
|
|
103
|
+
if (place === 'top') {
|
|
104
|
+
top = targetRect.top - tooltipRect.height - 8
|
|
105
|
+
} else {
|
|
106
|
+
top = targetRect.bottom + 8
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// left or right: vertical alignment
|
|
110
|
+
if (popperPosition.value === 'center') {
|
|
111
|
+
top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2
|
|
112
|
+
} else if (popperPosition.value === 'end') {
|
|
113
|
+
top = targetRect.bottom - tooltipRect.height
|
|
114
|
+
} else {
|
|
115
|
+
top = targetRect.top
|
|
116
|
+
}
|
|
149
117
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
118
|
+
if (place === 'left') {
|
|
119
|
+
left = targetRect.left - tooltipRect.width - 8
|
|
120
|
+
} else {
|
|
121
|
+
left = targetRect.right + 8
|
|
122
|
+
}
|
|
123
|
+
}
|
|
154
124
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
125
|
+
// keep tooltip inside the viewport (small clamp)
|
|
126
|
+
const minGap = 6
|
|
127
|
+
if (left < minGap) left = minGap
|
|
128
|
+
if (left + tooltipRect.width > vw - minGap) left = vw - tooltipRect.width - minGap
|
|
129
|
+
if (top < minGap) top = minGap
|
|
130
|
+
if (top + tooltipRect.height > vh - minGap) top = vh - tooltipRect.height - minGap
|
|
158
131
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
}
|
|
132
|
+
tooltipStyle.value.left = `${Math.round(left)}px`
|
|
133
|
+
tooltipStyle.value.top = `${Math.round(top)}px`
|
|
134
|
+
tooltipStyle.value.transform = 'none'
|
|
165
135
|
}
|
|
166
136
|
|
|
167
|
-
|
|
137
|
+
/* --- events --- */
|
|
138
|
+
function handleMouseEnter(e: MouseEvent) {
|
|
139
|
+
// clear close timeout
|
|
168
140
|
if (closeTimeout) {
|
|
169
141
|
clearTimeout(closeTimeout)
|
|
170
142
|
closeTimeout = null
|
|
171
143
|
}
|
|
172
|
-
if (open.value) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
144
|
|
|
176
|
-
|
|
145
|
+
const targetEl = (e.currentTarget ?? e.target) as HTMLElement
|
|
146
|
+
|
|
147
|
+
// schedule open
|
|
148
|
+
openTimeout = openTimeout || setTimeout(async () => {
|
|
177
149
|
open.value = true
|
|
150
|
+
await nextTick()
|
|
151
|
+
// measure and position after tooltip is rendered
|
|
152
|
+
updatePosition(targetEl)
|
|
178
153
|
openTimeout = null
|
|
179
154
|
}, props.openDelay)
|
|
180
155
|
}
|
|
181
156
|
|
|
182
|
-
|
|
157
|
+
function handleMouseLeave(e?: MouseEvent) {
|
|
183
158
|
if (openTimeout) {
|
|
184
159
|
clearTimeout(openTimeout)
|
|
185
160
|
openTimeout = null
|
|
186
161
|
}
|
|
187
|
-
if (!open.value)
|
|
188
|
-
|
|
189
|
-
}
|
|
162
|
+
if (!open.value) return
|
|
163
|
+
|
|
190
164
|
closeTimeout = closeTimeout || setTimeout(() => {
|
|
191
165
|
open.value = false
|
|
192
166
|
closeTimeout = null
|
|
193
167
|
}, props.closeDelay)
|
|
194
168
|
}
|
|
169
|
+
|
|
170
|
+
// if tooltip is open and user scrolls/resizes, recalc
|
|
171
|
+
function onWindowChange() {
|
|
172
|
+
// try to recalc against the wrapper's slot element if present
|
|
173
|
+
// we assume wrapper contains the hovered child
|
|
174
|
+
const hovered = wrapper.value?.querySelector(':hover') as HTMLElement | null
|
|
175
|
+
const target = hovered ?? wrapper.value
|
|
176
|
+
updatePosition(target)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onMounted(() => {
|
|
180
|
+
window.addEventListener('resize', onWindowChange)
|
|
181
|
+
window.addEventListener('scroll', onWindowChange, true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
onBeforeUnmount(() => {
|
|
185
|
+
window.removeEventListener('resize', onWindowChange)
|
|
186
|
+
window.removeEventListener('scroll', onWindowChange, true)
|
|
187
|
+
})
|
|
188
|
+
|
|
195
189
|
</script>
|
|
196
190
|
|
|
197
191
|
<template>
|
|
198
192
|
<div
|
|
193
|
+
ref="wrapper"
|
|
199
194
|
v-bind="$attrs"
|
|
200
|
-
|
|
201
|
-
@
|
|
202
|
-
@mouseleave="
|
|
203
|
-
@mouseenter="onMouseEnter"
|
|
195
|
+
class="relative"
|
|
196
|
+
@mouseenter="handleMouseEnter"
|
|
197
|
+
@mouseleave="handleMouseLeave"
|
|
204
198
|
>
|
|
205
199
|
<slot :open="open">
|
|
206
200
|
Hover
|
|
207
201
|
</slot>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
>
|
|
202
|
+
|
|
203
|
+
<!-- tooltip rendered absolutely on body of component, positioned by inline styles -->
|
|
204
|
+
<transition name="fade" appear>
|
|
212
205
|
<div
|
|
213
|
-
v-if="open && !prevent"
|
|
214
|
-
|
|
215
|
-
:style="
|
|
206
|
+
v-if="open && !props.prevent"
|
|
207
|
+
class="pointer-events-none"
|
|
208
|
+
:style="{ position: 'fixed', left: tooltipStyle.left, top: tooltipStyle.top, zIndex: props.zIndex ?? 9999 }"
|
|
216
209
|
>
|
|
217
|
-
<div
|
|
218
|
-
v-if="
|
|
219
|
-
:class="poperClasses"
|
|
220
|
-
/>
|
|
221
|
-
<div :class="baseClasses" ref="body">
|
|
222
|
-
<div
|
|
223
|
-
v-if="hasHeader"
|
|
224
|
-
class="font-semibold mb-1"
|
|
225
|
-
>
|
|
210
|
+
<div ref="body" :class="baseClass">
|
|
211
|
+
<div v-if="$slots.header || props.header" class="font-semibold mb-1">
|
|
226
212
|
<slot name="header">
|
|
227
|
-
<p>{{ header }}</p>
|
|
213
|
+
<p>{{ props.header }}</p>
|
|
228
214
|
</slot>
|
|
229
215
|
</div>
|
|
230
216
|
|
|
231
217
|
<slot name="content">
|
|
232
|
-
{{ text }}
|
|
218
|
+
{{ props.text }}
|
|
233
219
|
</slot>
|
|
234
220
|
</div>
|
|
235
221
|
</div>
|
|
236
|
-
</
|
|
222
|
+
</transition>
|
|
237
223
|
</div>
|
|
238
224
|
</template>
|
|
239
225
|
|
|
240
226
|
<style scoped>
|
|
241
|
-
|
|
227
|
+
/* simple fade (you can adapt to your transition) */
|
|
228
|
+
.fade-enter-active, .fade-leave-active {
|
|
229
|
+
transition: opacity .18s ease;
|
|
230
|
+
}
|
|
231
|
+
.fade-enter-from, .fade-leave-to {
|
|
232
|
+
opacity: 0;
|
|
233
|
+
}
|
|
234
|
+
.fade-enter-to, .fade-leave-from {
|
|
235
|
+
opacity: 1;
|
|
236
|
+
}
|
|
242
237
|
</style>
|