pgo-ui 1.0.38 → 1.0.39
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/dist/Radio-BVlsk-n_.js +4 -0
- package/dist/{index-piz7IhPQ.js → index-A5K9EQi-.js} +5848 -5813
- package/dist/index.es.js +39 -38
- package/dist/index.umd.js +40 -40
- package/dist/pgo-ui.css +1 -1
- package/package.json +1 -1
- package/src/components/pgo/DocumentViewer.vue +14 -74
- package/src/components/pgo/Tooltip.vue +52 -28
- package/src/components/pgo/__Tooltip.vue +281 -0
- package/src/components/pgo/forms/DynamicForm.vue +6 -1
- package/src/components/pgo/index.ts +2 -0
- package/src/components/pgo/inputs/LabelField.vue +8 -7
- package/src/pgo-components/directives/tooltip-directive.ts +39 -19
- package/src/pgo-components/pages/ListView.vue +1 -1
- package/src/pgo-components/services/axios.js +0 -1
- package/src/pgo-components/styles/global.css +2 -1
- package/dist/Radio-BmYjwUj3.js +0 -4
package/package.json
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
<!-- Iframe PDF viewer -->
|
|
50
50
|
<iframe
|
|
51
|
-
v-if="
|
|
51
|
+
v-if="src && open && !error"
|
|
52
52
|
ref="iframeRef"
|
|
53
53
|
:src="iframeSrc"
|
|
54
54
|
class="w-full h-full border-0"
|
|
@@ -85,80 +85,19 @@ const props = defineProps({
|
|
|
85
85
|
|
|
86
86
|
const emit = defineEmits(['update:modelValue', 'loaded', 'error', 'close'])
|
|
87
87
|
|
|
88
|
-
const open
|
|
89
|
-
const iframeRef
|
|
90
|
-
const loading
|
|
91
|
-
const error
|
|
92
|
-
const validatedSrc = ref('')
|
|
93
|
-
|
|
94
|
-
const DOCUMENT_TYPES = [
|
|
95
|
-
'application/pdf',
|
|
96
|
-
'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml',
|
|
97
|
-
'text/plain',
|
|
98
|
-
]
|
|
88
|
+
const open = ref(props.modelValue)
|
|
89
|
+
const iframeRef = ref(null)
|
|
90
|
+
const loading = ref(true)
|
|
91
|
+
const error = ref(null)
|
|
99
92
|
|
|
100
93
|
// Build iframe src — hide native toolbar so only our custom buttons are used
|
|
101
94
|
const iframeSrc = computed(() => {
|
|
102
|
-
if (!
|
|
95
|
+
if (!props.src) return ''
|
|
103
96
|
const params = ['toolbar=0', 'navpanes=0']
|
|
104
97
|
if (props.initialPage > 1) params.push(`page=${props.initialPage}`)
|
|
105
|
-
return `${
|
|
98
|
+
return `${props.src}#${params.join('&')}`
|
|
106
99
|
})
|
|
107
100
|
|
|
108
|
-
// ─── Validate source URL ──────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
function isValidUrl(str) {
|
|
111
|
-
if (/^https?:\/\//i.test(str)) return true
|
|
112
|
-
if (str.startsWith('/')) return true
|
|
113
|
-
try { return Boolean(new URL(str)) } catch { return false }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function validateSrc(url) {
|
|
117
|
-
if (!url) {
|
|
118
|
-
error.value = 'No document source provided'
|
|
119
|
-
loading.value = false
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!isValidUrl(url)) {
|
|
124
|
-
error.value = 'Invalid document URL'
|
|
125
|
-
loading.value = false
|
|
126
|
-
emit('error', error.value)
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
loading.value = true
|
|
131
|
-
error.value = null
|
|
132
|
-
validatedSrc.value = ''
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const res = await fetch(url, { method: 'HEAD' })
|
|
136
|
-
|
|
137
|
-
if (!res.ok) {
|
|
138
|
-
error.value = res.status === 404
|
|
139
|
-
? 'Document not found'
|
|
140
|
-
: `Failed to load document (${res.status})`
|
|
141
|
-
loading.value = false
|
|
142
|
-
emit('error', error.value)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
|
|
147
|
-
if (contentType && !DOCUMENT_TYPES.some(t => contentType.startsWith(t))) {
|
|
148
|
-
error.value = `Unsupported file type: ${contentType}`
|
|
149
|
-
loading.value = false
|
|
150
|
-
emit('error', error.value)
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
validatedSrc.value = url
|
|
155
|
-
} catch {
|
|
156
|
-
error.value = 'Unable to reach the document. Please check the URL or your connection.'
|
|
157
|
-
loading.value = false
|
|
158
|
-
emit('error', error.value)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
101
|
// ─── Sync modelValue ──────────────────────────────────────────────────────────
|
|
163
102
|
|
|
164
103
|
watch(() => props.modelValue, (val) => { open.value = val })
|
|
@@ -166,11 +105,11 @@ watch(() => props.modelValue, (val) => { open.value = val })
|
|
|
166
105
|
watch(open, (val) => {
|
|
167
106
|
emit('update:modelValue', val)
|
|
168
107
|
if (val) {
|
|
169
|
-
|
|
108
|
+
loading.value = true
|
|
109
|
+
error.value = null
|
|
170
110
|
} else {
|
|
171
|
-
loading.value
|
|
172
|
-
error.value
|
|
173
|
-
validatedSrc.value = ''
|
|
111
|
+
loading.value = false
|
|
112
|
+
error.value = null
|
|
174
113
|
}
|
|
175
114
|
})
|
|
176
115
|
|
|
@@ -222,8 +161,9 @@ function printPdf() {
|
|
|
222
161
|
}
|
|
223
162
|
}
|
|
224
163
|
|
|
225
|
-
watch(() => props.src, (
|
|
226
|
-
|
|
164
|
+
watch(() => props.src, () => {
|
|
165
|
+
loading.value = true
|
|
166
|
+
error.value = null
|
|
227
167
|
})
|
|
228
168
|
|
|
229
169
|
defineExpose({ printPdf, downloadPdf })
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
<Teleport to="body">
|
|
18
18
|
<div
|
|
19
|
-
v-show="isOpen"
|
|
19
|
+
v-show="isOpen && hasPositioned"
|
|
20
20
|
:id="tooltipId"
|
|
21
21
|
ref="tooltipRef"
|
|
22
|
-
class="vts-
|
|
22
|
+
class="vts-bg-surface-elevated vts-text vts-border vts-rounded-sm vts-elevation-2 vts-pt-2 vts-pb-2 vts-pl-3 vts-pr-3 vts-max-w-280 tooltip-container"
|
|
23
23
|
role="tooltip"
|
|
24
24
|
:class="[placementClass]"
|
|
25
25
|
:style="tooltipStyles"
|
|
@@ -109,25 +109,23 @@
|
|
|
109
109
|
const activator = activatorRef.value
|
|
110
110
|
const tooltip = tooltipRef.value
|
|
111
111
|
if (!activator || !tooltip) return
|
|
112
|
-
|
|
112
|
+
|
|
113
|
+
// Measure tooltip off-screen first
|
|
113
114
|
if (!hasPositioned.value) {
|
|
114
115
|
tooltipStyles.value = {
|
|
115
|
-
position: '
|
|
116
|
+
position: 'fixed',
|
|
116
117
|
top: '-9999px',
|
|
117
118
|
left: '-9999px',
|
|
118
119
|
visibility: 'hidden',
|
|
119
|
-
zIndex: 'var(--vts-z-tooltip, 1000)'
|
|
120
|
+
zIndex: 'var(--vts-z-tooltip, 1000)',
|
|
121
|
+
pointerEvents: 'none'
|
|
120
122
|
}
|
|
121
|
-
// Force a reflow so getBoundingClientRect has correct size
|
|
122
123
|
void tooltip.offsetHeight
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
const rect = activator.getBoundingClientRect()
|
|
126
127
|
const tooltipRect = tooltip.getBoundingClientRect()
|
|
127
128
|
|
|
128
|
-
const scrollX = window.scrollX || window.pageXOffset
|
|
129
|
-
const scrollY = window.scrollY || window.pageYOffset
|
|
130
|
-
|
|
131
129
|
let top = 0
|
|
132
130
|
let left = 0
|
|
133
131
|
|
|
@@ -135,50 +133,71 @@
|
|
|
135
133
|
const offsetY = props.offset?.y ?? 0
|
|
136
134
|
const gap = props.arrow ? 8 : 0
|
|
137
135
|
|
|
136
|
+
// Position relative to viewport (fixed positioning)
|
|
138
137
|
switch (normalizedPlacement.value) {
|
|
139
138
|
case 'top':
|
|
140
|
-
top = rect.top
|
|
141
|
-
left = rect.left +
|
|
139
|
+
top = rect.top - tooltipRect.height - gap - offsetY
|
|
140
|
+
left = rect.left + rect.width / 2 - tooltipRect.width / 2 + offsetX
|
|
142
141
|
break
|
|
143
142
|
case 'bottom':
|
|
144
|
-
top = rect.bottom +
|
|
145
|
-
left = rect.left +
|
|
143
|
+
top = rect.bottom + gap + offsetY
|
|
144
|
+
left = rect.left + rect.width / 2 - tooltipRect.width / 2 + offsetX
|
|
146
145
|
break
|
|
147
146
|
case 'left':
|
|
148
|
-
top = rect.top +
|
|
149
|
-
left = rect.left
|
|
147
|
+
top = rect.top + rect.height / 2 - tooltipRect.height / 2 + offsetY
|
|
148
|
+
left = rect.left - tooltipRect.width - gap - offsetX
|
|
150
149
|
break
|
|
151
150
|
case 'right':
|
|
152
|
-
top = rect.top +
|
|
153
|
-
left = rect.right +
|
|
151
|
+
top = rect.top + rect.height / 2 - tooltipRect.height / 2 + offsetY
|
|
152
|
+
left = rect.right + gap + offsetX
|
|
154
153
|
break
|
|
155
154
|
}
|
|
156
155
|
|
|
156
|
+
// Clamp to viewport so the tooltip never causes scrollbars
|
|
157
|
+
const viewW = document.documentElement.clientWidth
|
|
158
|
+
const viewH = document.documentElement.clientHeight
|
|
159
|
+
const pad = 8
|
|
160
|
+
|
|
161
|
+
left = Math.max(pad, Math.min(left, viewW - tooltipRect.width - pad))
|
|
162
|
+
top = Math.max(pad, Math.min(top, viewH - tooltipRect.height - pad))
|
|
163
|
+
|
|
157
164
|
tooltipStyles.value = {
|
|
158
|
-
position: '
|
|
165
|
+
position: 'fixed',
|
|
159
166
|
top: `${top}px`,
|
|
160
167
|
left: `${left}px`,
|
|
161
168
|
zIndex: 'var(--vts-z-tooltip, 1000)',
|
|
162
|
-
visibility: ''
|
|
169
|
+
visibility: '',
|
|
170
|
+
pointerEvents: 'auto'
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
// Arrow positioning
|
|
173
|
+
// Arrow positioning — offset arrow to point at the activator center
|
|
166
174
|
if (props.arrow) {
|
|
167
175
|
const size = 8
|
|
168
176
|
const common = { width: `${size}px`, height: `${size}px` }
|
|
177
|
+
const actCenterX = rect.left + rect.width / 2
|
|
178
|
+
const actCenterY = rect.top + rect.height / 2
|
|
179
|
+
const arrowMinEdge = 12
|
|
169
180
|
switch (normalizedPlacement.value) {
|
|
170
|
-
case 'top':
|
|
171
|
-
|
|
181
|
+
case 'top': {
|
|
182
|
+
const ax = Math.max(arrowMinEdge, Math.min(actCenterX - left, tooltipRect.width - arrowMinEdge))
|
|
183
|
+
arrowStyles.value = { ...common, bottom: `-4px`, left: `${ax}px`, transform: 'translateX(-50%) rotate(45deg)' }
|
|
172
184
|
break
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
}
|
|
186
|
+
case 'bottom': {
|
|
187
|
+
const ax = Math.max(arrowMinEdge, Math.min(actCenterX - left, tooltipRect.width - arrowMinEdge))
|
|
188
|
+
arrowStyles.value = { ...common, top: `-4px`, left: `${ax}px`, transform: 'translateX(-50%) rotate(45deg)' }
|
|
175
189
|
break
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
}
|
|
191
|
+
case 'left': {
|
|
192
|
+
const ay = Math.max(arrowMinEdge, Math.min(actCenterY - top, tooltipRect.height - arrowMinEdge))
|
|
193
|
+
arrowStyles.value = { ...common, right: `-4px`, top: `${ay}px`, transform: 'translateY(-50%) rotate(45deg)' }
|
|
178
194
|
break
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
}
|
|
196
|
+
case 'right': {
|
|
197
|
+
const ay = Math.max(arrowMinEdge, Math.min(actCenterY - top, tooltipRect.height - arrowMinEdge))
|
|
198
|
+
arrowStyles.value = { ...common, left: `-4px`, top: `${ay}px`, transform: 'translateY(-50%) rotate(45deg)' }
|
|
181
199
|
break
|
|
200
|
+
}
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
203
|
|
|
@@ -278,4 +297,9 @@
|
|
|
278
297
|
</script>
|
|
279
298
|
|
|
280
299
|
<style scoped>
|
|
300
|
+
.tooltip-container {
|
|
301
|
+
position: fixed;
|
|
302
|
+
pointer-events: none;
|
|
303
|
+
max-width: calc(100vw - 16px);
|
|
304
|
+
}
|
|
281
305
|
</style>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="activatorRef"
|
|
4
|
+
class="vts-d-inline-block"
|
|
5
|
+
@mouseenter="onMouseEnter"
|
|
6
|
+
@mouseleave="onMouseLeave"
|
|
7
|
+
@focusin="onFocusIn"
|
|
8
|
+
@focusout="onFocusOut"
|
|
9
|
+
@click="onClick"
|
|
10
|
+
:aria-describedby="tooltipId"
|
|
11
|
+
:aria-expanded="isOpen"
|
|
12
|
+
:aria-disabled="disabled ? 'true' : 'false'"
|
|
13
|
+
>
|
|
14
|
+
<slot name="activator" :props="activatorSlotProps"></slot>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<Teleport to="body">
|
|
18
|
+
<div
|
|
19
|
+
v-show="isOpen"
|
|
20
|
+
:id="tooltipId"
|
|
21
|
+
ref="tooltipRef"
|
|
22
|
+
class="vts-position-absolute vts-bg-surface-elevated vts-text vts-border vts-rounded-sm vts-elevation-2 vts-pt-2 vts-pb-2 vts-pl-3 vts-pr-3 vts-max-w-280"
|
|
23
|
+
role="tooltip"
|
|
24
|
+
:class="[placementClass]"
|
|
25
|
+
:style="tooltipStyles"
|
|
26
|
+
@keydown.esc.stop.prevent="close"
|
|
27
|
+
>
|
|
28
|
+
<div v-if="arrow" class="vts-position-absolute vts-bg-surface-elevated vts-border-t vts-border-s vts-border-color" :class="[placementClass]" :style="arrowStyles"></div>
|
|
29
|
+
<div class="vts-text-body-2">
|
|
30
|
+
<slot />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</Teleport>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
38
|
+
import { globalRtl } from '../../pgo-components/lib/core/rtl/rtl'
|
|
39
|
+
|
|
40
|
+
// Props
|
|
41
|
+
interface Offset {
|
|
42
|
+
x?: number
|
|
43
|
+
y?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Trigger = 'hover' | 'focus' | 'click' | 'manual'
|
|
47
|
+
|
|
48
|
+
type PlacementBase = 'top' | 'bottom' | 'left' | 'right'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
modelValue: { type: Boolean, default: undefined }, // controlled mode
|
|
53
|
+
openOnHover: { type: Boolean, default: true },
|
|
54
|
+
openOnFocus: { type: Boolean, default: true },
|
|
55
|
+
openOnClick: { type: Boolean, default: false },
|
|
56
|
+
disabled: { type: Boolean, default: false },
|
|
57
|
+
placement: { type: String as () => PlacementBase, default: 'top' },
|
|
58
|
+
offset: { type: Object as () => Offset, default: () => ({ x: 0, y: 0 }) },
|
|
59
|
+
arrow: { type: Boolean, default: true },
|
|
60
|
+
showDelay: { type: Number, default: 80 },
|
|
61
|
+
hideDelay: { type: Number, default: 80 },
|
|
62
|
+
teleport: { type: String, default: 'body' }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const emit = defineEmits(['update:modelValue', 'open', 'close'])
|
|
66
|
+
|
|
67
|
+
// State
|
|
68
|
+
const isControlled = computed(() => props.modelValue !== undefined)
|
|
69
|
+
const internalOpen = ref(false)
|
|
70
|
+
const isOpen = computed({
|
|
71
|
+
get: () => (isControlled.value ? !!props.modelValue : internalOpen.value),
|
|
72
|
+
set: (val: boolean) => {
|
|
73
|
+
if (props.disabled) return
|
|
74
|
+
if (isControlled.value) emit('update:modelValue', val)
|
|
75
|
+
else internalOpen.value = val
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Elements
|
|
80
|
+
const activatorRef = ref<HTMLElement | null>(null)
|
|
81
|
+
const tooltipRef = ref<HTMLElement | null>(null)
|
|
82
|
+
|
|
83
|
+
// IDs
|
|
84
|
+
const tooltipId = `tooltip-${Math.random().toString(36).slice(2, 10)}`
|
|
85
|
+
|
|
86
|
+
// RTL
|
|
87
|
+
const isRtl = computed(() => globalRtl.value)
|
|
88
|
+
|
|
89
|
+
// Placement with RTL adjustment for left/right
|
|
90
|
+
const normalizedPlacement = computed<PlacementBase>(() => {
|
|
91
|
+
if (!isRtl.value) return props.placement
|
|
92
|
+
if (props.placement === 'left') return 'right'
|
|
93
|
+
if (props.placement === 'right') return 'left'
|
|
94
|
+
return props.placement
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const placementClass = computed(() => `placement-${normalizedPlacement.value}`)
|
|
98
|
+
|
|
99
|
+
// Positioning
|
|
100
|
+
const tooltipStyles = ref<Record<string, string>>({})
|
|
101
|
+
const arrowStyles = ref<Record<string, string>>({})
|
|
102
|
+
const hasPositioned = ref(false)
|
|
103
|
+
|
|
104
|
+
// Track how tooltip was opened to coordinate closing behavior
|
|
105
|
+
type TriggerSource = 'hover' | 'focus' | 'click' | 'manual' | null
|
|
106
|
+
const lastTrigger = ref<TriggerSource>(null)
|
|
107
|
+
|
|
108
|
+
function computePosition() {
|
|
109
|
+
const activator = activatorRef.value
|
|
110
|
+
const tooltip = tooltipRef.value
|
|
111
|
+
if (!activator || !tooltip) return
|
|
112
|
+
// Ensure the tooltip can be measured on first render
|
|
113
|
+
if (!hasPositioned.value) {
|
|
114
|
+
tooltipStyles.value = {
|
|
115
|
+
position: 'absolute',
|
|
116
|
+
top: '-9999px',
|
|
117
|
+
left: '-9999px',
|
|
118
|
+
visibility: 'hidden',
|
|
119
|
+
zIndex: 'var(--vts-z-tooltip, 1000)'
|
|
120
|
+
}
|
|
121
|
+
// Force a reflow so getBoundingClientRect has correct size
|
|
122
|
+
void tooltip.offsetHeight
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rect = activator.getBoundingClientRect()
|
|
126
|
+
const tooltipRect = tooltip.getBoundingClientRect()
|
|
127
|
+
|
|
128
|
+
const scrollX = window.scrollX || window.pageXOffset
|
|
129
|
+
const scrollY = window.scrollY || window.pageYOffset
|
|
130
|
+
|
|
131
|
+
let top = 0
|
|
132
|
+
let left = 0
|
|
133
|
+
|
|
134
|
+
const offsetX = props.offset?.x ?? 0
|
|
135
|
+
const offsetY = props.offset?.y ?? 0
|
|
136
|
+
const gap = props.arrow ? 8 : 0
|
|
137
|
+
|
|
138
|
+
switch (normalizedPlacement.value) {
|
|
139
|
+
case 'top':
|
|
140
|
+
top = rect.top + scrollY - tooltipRect.height - gap - offsetY
|
|
141
|
+
left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 + offsetX
|
|
142
|
+
break
|
|
143
|
+
case 'bottom':
|
|
144
|
+
top = rect.bottom + scrollY + gap + offsetY
|
|
145
|
+
left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 + offsetX
|
|
146
|
+
break
|
|
147
|
+
case 'left':
|
|
148
|
+
top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 + offsetY
|
|
149
|
+
left = rect.left + scrollX - tooltipRect.width - gap - offsetX
|
|
150
|
+
break
|
|
151
|
+
case 'right':
|
|
152
|
+
top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 + offsetY
|
|
153
|
+
left = rect.right + scrollX + gap + offsetX
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
tooltipStyles.value = {
|
|
158
|
+
position: 'absolute',
|
|
159
|
+
top: `${top}px`,
|
|
160
|
+
left: `${left}px`,
|
|
161
|
+
zIndex: 'var(--vts-z-tooltip, 1000)',
|
|
162
|
+
visibility: ''
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Arrow positioning
|
|
166
|
+
if (props.arrow) {
|
|
167
|
+
const size = 8
|
|
168
|
+
const common = { width: `${size}px`, height: `${size}px` }
|
|
169
|
+
switch (normalizedPlacement.value) {
|
|
170
|
+
case 'top':
|
|
171
|
+
arrowStyles.value = { ...common, bottom: `-4px`, left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
|
|
172
|
+
break
|
|
173
|
+
case 'bottom':
|
|
174
|
+
arrowStyles.value = { ...common, top: `-4px`, left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
|
|
175
|
+
break
|
|
176
|
+
case 'left':
|
|
177
|
+
arrowStyles.value = { ...common, right: `-4px`, top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
|
|
178
|
+
break
|
|
179
|
+
case 'right':
|
|
180
|
+
arrowStyles.value = { ...common, left: `-4px`, top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
hasPositioned.value = true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Open/close with delays
|
|
189
|
+
let showTimer: number | null = null
|
|
190
|
+
let hideTimer: number | null = null
|
|
191
|
+
|
|
192
|
+
function clearTimers() {
|
|
193
|
+
if (showTimer) {
|
|
194
|
+
window.clearTimeout(showTimer)
|
|
195
|
+
showTimer = null
|
|
196
|
+
}
|
|
197
|
+
if (hideTimer) {
|
|
198
|
+
window.clearTimeout(hideTimer)
|
|
199
|
+
hideTimer = null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function open(source: TriggerSource = null) {
|
|
204
|
+
if (props.disabled) return
|
|
205
|
+
clearTimers()
|
|
206
|
+
showTimer = window.setTimeout(async () => {
|
|
207
|
+
lastTrigger.value = source
|
|
208
|
+
isOpen.value = true
|
|
209
|
+
await nextTick()
|
|
210
|
+
computePosition()
|
|
211
|
+
emit('open')
|
|
212
|
+
}, props.showDelay)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function close() {
|
|
216
|
+
clearTimers()
|
|
217
|
+
hideTimer = window.setTimeout(() => {
|
|
218
|
+
isOpen.value = false
|
|
219
|
+
lastTrigger.value = null
|
|
220
|
+
hasPositioned.value = false
|
|
221
|
+
emit('close')
|
|
222
|
+
}, props.hideDelay)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Triggers
|
|
226
|
+
const activatorSlotProps = computed(() => ({
|
|
227
|
+
open: (src?: TriggerSource) => open(src ?? 'manual'),
|
|
228
|
+
close,
|
|
229
|
+
isOpen: isOpen.value
|
|
230
|
+
}))
|
|
231
|
+
|
|
232
|
+
function onMouseEnter() {
|
|
233
|
+
if (props.openOnHover) open('hover')
|
|
234
|
+
}
|
|
235
|
+
function onMouseLeave() {
|
|
236
|
+
if (props.openOnHover) close()
|
|
237
|
+
}
|
|
238
|
+
function onFocusIn() {
|
|
239
|
+
if (props.openOnFocus) open('focus')
|
|
240
|
+
}
|
|
241
|
+
function onFocusOut() {
|
|
242
|
+
// Avoid immediate close on click-first interaction when openOnClick is enabled
|
|
243
|
+
if (props.openOnFocus && lastTrigger.value === 'focus') close()
|
|
244
|
+
}
|
|
245
|
+
function onClick() {
|
|
246
|
+
if (!props.openOnClick) return
|
|
247
|
+
if (isOpen.value) close()
|
|
248
|
+
else open('click')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Outside click & escape
|
|
252
|
+
function onDocumentClick(e: MouseEvent) {
|
|
253
|
+
if (!isOpen.value) return
|
|
254
|
+
const target = e.target as Node
|
|
255
|
+
if (tooltipRef.value && tooltipRef.value.contains(target)) return
|
|
256
|
+
if (activatorRef.value && activatorRef.value.contains(target)) return
|
|
257
|
+
close()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function onDocumentKeydown(e: KeyboardEvent) {
|
|
261
|
+
if (e.key === 'Escape') close()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
onMounted(() => {
|
|
265
|
+
computePosition()
|
|
266
|
+
document.addEventListener('click', onDocumentClick, { passive: true })
|
|
267
|
+
document.addEventListener('keydown', onDocumentKeydown)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
onBeforeUnmount(() => {
|
|
271
|
+
document.removeEventListener('click', onDocumentClick)
|
|
272
|
+
document.removeEventListener('keydown', onDocumentKeydown)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
watch(isOpen, val => {
|
|
276
|
+
if (val) nextTick(computePosition)
|
|
277
|
+
})
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<style scoped>
|
|
281
|
+
</style>
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
type="hidden"
|
|
21
21
|
/>
|
|
22
22
|
<!-- Render grouped fields -->
|
|
23
|
+
|
|
23
24
|
<!-- -->
|
|
24
25
|
<template v-if="form.groups">
|
|
25
26
|
<template v-for="(group, id) in form.groups" :key="id">
|
|
@@ -123,7 +124,7 @@
|
|
|
123
124
|
import { ref, computed, watch, defineAsyncComponent, inject, reactive, onMounted, nextTick } from 'vue'
|
|
124
125
|
import Form from './Form.vue';
|
|
125
126
|
import Modal from '../Modal.vue'
|
|
126
|
-
import { Banner } from '../index'
|
|
127
|
+
import { Banner, LabelField } from '../index'
|
|
127
128
|
import { gridColsMap, columnSpansMap, useLanguageSelected, initializeFunctions } from '@/pgo-components/lib/componentConfig';
|
|
128
129
|
|
|
129
130
|
const rules = inject('validationRules')
|
|
@@ -522,6 +523,10 @@
|
|
|
522
523
|
map['file'] = defineAsyncComponent(() => import('../inputs/FileUpload.vue'))
|
|
523
524
|
map['filefield'] = defineAsyncComponent(() => import('../inputs/FileUpload.vue'))
|
|
524
525
|
}
|
|
526
|
+
if (usedTypes.has('label') || usedTypes.has('labelfield')) {
|
|
527
|
+
map['label'] = defineAsyncComponent(() => import('../inputs/LabelField.vue'))
|
|
528
|
+
map['labelfield'] = defineAsyncComponent(() => import('../inputs/LabelField.vue'))
|
|
529
|
+
}
|
|
525
530
|
|
|
526
531
|
return map
|
|
527
532
|
})
|
|
@@ -22,6 +22,7 @@ import FileUpload from './inputs/FileUpload.vue'
|
|
|
22
22
|
import FilterSection from './filters/FilterSection.vue'
|
|
23
23
|
import Footer from './Footer.vue'
|
|
24
24
|
import Form from './forms/Form.vue'
|
|
25
|
+
import LabelField from './inputs/LabelField.vue'
|
|
25
26
|
import LayoutContainer from './LayoutContainer.vue'
|
|
26
27
|
import Main from './Main.vue'
|
|
27
28
|
import Modal from './Modal.vue'
|
|
@@ -70,6 +71,7 @@ export { Button, Card, HeroIcon,
|
|
|
70
71
|
FilterSection,
|
|
71
72
|
Footer,
|
|
72
73
|
Form,
|
|
74
|
+
LabelField,
|
|
73
75
|
LayoutContainer,
|
|
74
76
|
Main,
|
|
75
77
|
Modal,
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<div :class="['flex items-start gap-2', width, margin]">
|
|
3
3
|
<!-- Label -->
|
|
4
4
|
<span
|
|
5
|
-
v-if="
|
|
5
|
+
v-if="label"
|
|
6
6
|
:class="['text-sm font-medium shrink-0', labelClass]"
|
|
7
7
|
>
|
|
8
|
-
{{
|
|
8
|
+
{{ useLanguageSelected(label, computedLang) }}:
|
|
9
9
|
</span>
|
|
10
10
|
|
|
11
11
|
<!-- Value -->
|
|
@@ -21,8 +21,8 @@ import { useLanguageSelected } from '../../../pgo-components/lib/componentConfig
|
|
|
21
21
|
|
|
22
22
|
const props = defineProps({
|
|
23
23
|
label: { type: [String, Object], default: '' },
|
|
24
|
-
labelEn: { type: String, default: '' },
|
|
25
|
-
labelDv: { type: String, default: '' },
|
|
24
|
+
// labelEn: { type: String, default: '' },
|
|
25
|
+
// labelDv: { type: String, default: '' },
|
|
26
26
|
value: { type: [String, Number, Boolean, Object, Array], default: '' },
|
|
27
27
|
placeholder: { type: String, default: '-' },
|
|
28
28
|
width: { type: String, default: 'w-full' },
|
|
@@ -35,14 +35,15 @@ const props = defineProps({
|
|
|
35
35
|
|
|
36
36
|
const cardDir = inject('parentDir', '')
|
|
37
37
|
const cardLang = inject('parentLang', '')
|
|
38
|
+
const { language } = inject('i18n')
|
|
38
39
|
|
|
39
|
-
const computedLang = computed(() => props.lang || cardLang)
|
|
40
|
+
const computedLang = computed(() => props.lang || cardLang || language.value || 'dv')
|
|
40
41
|
|
|
41
42
|
const { selectedLabel } = useLanguageSelected(
|
|
42
43
|
computed(() => ({
|
|
43
44
|
label: props.label,
|
|
44
|
-
labelEn: props.labelEn,
|
|
45
|
-
labelDv: props.labelDv,
|
|
45
|
+
// labelEn: props.labelEn,
|
|
46
|
+
// labelDv: props.labelDv,
|
|
46
47
|
})),
|
|
47
48
|
computedLang
|
|
48
49
|
)
|