@wishbone-media/spark 0.36.0 → 0.38.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/dist/index.js +7772 -3148
- package/package.json +3 -1
- package/src/assets/css/spark-fancybox.css +7 -0
- package/src/assets/css/spark-tooltip.css +44 -0
- package/src/components/SparkEntityBadge.vue +175 -0
- package/src/components/SparkFancybox.vue +59 -0
- package/src/components/SparkImageUpload.vue +4 -3
- package/src/components/SparkTooltip.vue +162 -0
- package/src/components/index.js +3 -0
- package/src/directives/sparkTooltip.js +228 -0
- package/src/plugins/fontawesome.js +8 -0
- package/src/plugins/index.js +2 -1
- package/src/plugins/tooltip.js +11 -0
- package/src/utils/sparkTable/renderers/image.js +20 -5
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { computePosition, offset, flip, shift, arrow } from '@floating-ui/vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v-spark-tooltip directive for adding tooltips to any element.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* v-spark-tooltip="'Simple text'"
|
|
8
|
+
* v-spark-tooltip="{ content: 'HTML <b>bold</b>', html: true }"
|
|
9
|
+
* v-spark-tooltip="{ content: 'Tip', placement: 'bottom', offset: 12, delay: { show: 500, hide: 100 } }"
|
|
10
|
+
*
|
|
11
|
+
* Empty/falsy content = no tooltip.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const TOOLTIP_CLASS = 'spark-tooltip'
|
|
15
|
+
const ARROW_CLASS = 'spark-tooltip-arrow'
|
|
16
|
+
|
|
17
|
+
function normalizeConfig(value) {
|
|
18
|
+
if (!value) return { content: '' }
|
|
19
|
+
if (typeof value === 'string') return { content: value }
|
|
20
|
+
return {
|
|
21
|
+
content: '',
|
|
22
|
+
placement: 'top',
|
|
23
|
+
offset: 8,
|
|
24
|
+
html: false,
|
|
25
|
+
delay: { show: 200, hide: 0 },
|
|
26
|
+
arrow: true,
|
|
27
|
+
...value,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createTooltipElement(config) {
|
|
32
|
+
const tooltip = document.createElement('div')
|
|
33
|
+
tooltip.className = TOOLTIP_CLASS
|
|
34
|
+
tooltip.setAttribute('role', 'tooltip')
|
|
35
|
+
|
|
36
|
+
if (config.html) {
|
|
37
|
+
tooltip.innerHTML = config.content
|
|
38
|
+
} else {
|
|
39
|
+
tooltip.textContent = config.content
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (config.arrow) {
|
|
43
|
+
const arrowEl = document.createElement('div')
|
|
44
|
+
arrowEl.className = ARROW_CLASS
|
|
45
|
+
tooltip.appendChild(arrowEl)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return tooltip
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function updatePosition(el, tooltip, config) {
|
|
52
|
+
const arrowEl = tooltip.querySelector(`.${ARROW_CLASS}`)
|
|
53
|
+
|
|
54
|
+
const middleware = [
|
|
55
|
+
offset(config.offset ?? 8),
|
|
56
|
+
flip(),
|
|
57
|
+
shift({ padding: 8 }),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
if (arrowEl) {
|
|
61
|
+
middleware.push(arrow({ element: arrowEl, padding: 5 }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { x, y, placement, middlewareData } = await computePosition(el, tooltip, {
|
|
65
|
+
placement: config.placement || 'top',
|
|
66
|
+
strategy: 'fixed',
|
|
67
|
+
middleware,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
Object.assign(tooltip.style, {
|
|
71
|
+
left: `${x}px`,
|
|
72
|
+
top: `${y}px`,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Position the arrow
|
|
76
|
+
if (arrowEl && middlewareData.arrow) {
|
|
77
|
+
const { x: arrowX, y: arrowY } = middlewareData.arrow
|
|
78
|
+
const side = placement.split('-')[0]
|
|
79
|
+
const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[side]
|
|
80
|
+
|
|
81
|
+
Object.assign(arrowEl.style, {
|
|
82
|
+
left: arrowX != null ? `${arrowX}px` : '',
|
|
83
|
+
top: arrowY != null ? `${arrowY}px` : '',
|
|
84
|
+
right: '',
|
|
85
|
+
bottom: '',
|
|
86
|
+
[staticSide]: '-4px',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function showTooltip(el) {
|
|
92
|
+
const state = el.__sparkTooltip
|
|
93
|
+
if (!state || !state.config.content) return
|
|
94
|
+
|
|
95
|
+
clearTimeout(state.hideTimeout)
|
|
96
|
+
|
|
97
|
+
const delay = typeof state.config.delay === 'number'
|
|
98
|
+
? state.config.delay
|
|
99
|
+
: state.config.delay?.show ?? 200
|
|
100
|
+
|
|
101
|
+
state.showTimeout = setTimeout(() => {
|
|
102
|
+
if (state.tooltipEl) return // Already visible
|
|
103
|
+
|
|
104
|
+
const tooltip = createTooltipElement(state.config)
|
|
105
|
+
tooltip.style.position = 'fixed'
|
|
106
|
+
tooltip.style.top = '0'
|
|
107
|
+
tooltip.style.left = '0'
|
|
108
|
+
document.body.appendChild(tooltip)
|
|
109
|
+
state.tooltipEl = tooltip
|
|
110
|
+
|
|
111
|
+
updatePosition(el, tooltip, state.config)
|
|
112
|
+
}, delay)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hideTooltip(el) {
|
|
116
|
+
const state = el.__sparkTooltip
|
|
117
|
+
if (!state) return
|
|
118
|
+
|
|
119
|
+
clearTimeout(state.showTimeout)
|
|
120
|
+
|
|
121
|
+
const delay = typeof state.config.delay === 'number'
|
|
122
|
+
? 0
|
|
123
|
+
: state.config.delay?.hide ?? 0
|
|
124
|
+
|
|
125
|
+
state.hideTimeout = setTimeout(() => {
|
|
126
|
+
if (state.tooltipEl) {
|
|
127
|
+
state.tooltipEl.remove()
|
|
128
|
+
state.tooltipEl = null
|
|
129
|
+
}
|
|
130
|
+
}, delay)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setupListeners(el) {
|
|
134
|
+
const onShow = () => showTooltip(el)
|
|
135
|
+
const onHide = () => hideTooltip(el)
|
|
136
|
+
|
|
137
|
+
el.addEventListener('mouseenter', onShow)
|
|
138
|
+
el.addEventListener('mouseleave', onHide)
|
|
139
|
+
el.addEventListener('focus', onShow)
|
|
140
|
+
el.addEventListener('blur', onHide)
|
|
141
|
+
|
|
142
|
+
el.__sparkTooltip.listeners = { onShow, onHide }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function removeListeners(el) {
|
|
146
|
+
const state = el.__sparkTooltip
|
|
147
|
+
if (!state?.listeners) return
|
|
148
|
+
|
|
149
|
+
el.removeEventListener('mouseenter', state.listeners.onShow)
|
|
150
|
+
el.removeEventListener('mouseleave', state.listeners.onHide)
|
|
151
|
+
el.removeEventListener('focus', state.listeners.onShow)
|
|
152
|
+
el.removeEventListener('blur', state.listeners.onHide)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cleanup(el) {
|
|
156
|
+
const state = el.__sparkTooltip
|
|
157
|
+
if (!state) return
|
|
158
|
+
|
|
159
|
+
clearTimeout(state.showTimeout)
|
|
160
|
+
clearTimeout(state.hideTimeout)
|
|
161
|
+
removeListeners(el)
|
|
162
|
+
|
|
163
|
+
if (state.tooltipEl) {
|
|
164
|
+
state.tooltipEl.remove()
|
|
165
|
+
state.tooltipEl = null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
delete el.__sparkTooltip
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const sparkTooltipDirective = {
|
|
172
|
+
mounted(el, binding) {
|
|
173
|
+
const config = normalizeConfig(binding.value)
|
|
174
|
+
el.__sparkTooltip = { config, tooltipEl: null, showTimeout: null, hideTimeout: null }
|
|
175
|
+
|
|
176
|
+
if (config.content) {
|
|
177
|
+
setupListeners(el)
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
updated(el, binding) {
|
|
182
|
+
const config = normalizeConfig(binding.value)
|
|
183
|
+
const state = el.__sparkTooltip
|
|
184
|
+
|
|
185
|
+
if (!state) {
|
|
186
|
+
// Was never initialized (content was empty on mount)
|
|
187
|
+
el.__sparkTooltip = { config, tooltipEl: null, showTimeout: null, hideTimeout: null }
|
|
188
|
+
if (config.content) setupListeners(el)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
state.config = config
|
|
193
|
+
|
|
194
|
+
// Update visible tooltip content
|
|
195
|
+
if (state.tooltipEl) {
|
|
196
|
+
const arrowEl = state.tooltipEl.querySelector(`.${ARROW_CLASS}`)
|
|
197
|
+
if (config.html) {
|
|
198
|
+
state.tooltipEl.innerHTML = config.content
|
|
199
|
+
} else {
|
|
200
|
+
state.tooltipEl.textContent = config.content
|
|
201
|
+
}
|
|
202
|
+
// Re-append arrow if it was removed by innerHTML/textContent
|
|
203
|
+
if (config.arrow) {
|
|
204
|
+
if (arrowEl) {
|
|
205
|
+
state.tooltipEl.appendChild(arrowEl)
|
|
206
|
+
} else {
|
|
207
|
+
const newArrow = document.createElement('div')
|
|
208
|
+
newArrow.className = ARROW_CLASS
|
|
209
|
+
state.tooltipEl.appendChild(newArrow)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
updatePosition(el, state.tooltipEl, config)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add/remove listeners if content became available/empty
|
|
216
|
+
if (config.content && !state.listeners) {
|
|
217
|
+
setupListeners(el)
|
|
218
|
+
} else if (!config.content && state.listeners) {
|
|
219
|
+
hideTooltip(el)
|
|
220
|
+
removeListeners(el)
|
|
221
|
+
state.listeners = null
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
unmounted(el) {
|
|
226
|
+
cleanup(el)
|
|
227
|
+
},
|
|
228
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { library, icon as faIconToSvg } from '@fortawesome/fontawesome-svg-core'
|
|
2
2
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
3
|
+
import { setupTooltip } from './tooltip.js'
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
faArrowLeftToLine,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
faAnglesRight,
|
|
59
60
|
faAnglesLeft,
|
|
60
61
|
faEyeDropper,
|
|
62
|
+
faCopy,
|
|
63
|
+
faArrowUpRightFromSquare,
|
|
61
64
|
} from '@fortawesome/pro-regular-svg-icons'
|
|
62
65
|
|
|
63
66
|
import {
|
|
@@ -128,6 +131,8 @@ export const Icons = {
|
|
|
128
131
|
farAnglesLeft: faAnglesLeft,
|
|
129
132
|
farEyeDropper: faEyeDropper,
|
|
130
133
|
farCircleDot: faCircleDot,
|
|
134
|
+
farCopy: faCopy,
|
|
135
|
+
farArrowUpRightFromSquare: faArrowUpRightFromSquare,
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/**
|
|
@@ -200,4 +205,7 @@ export function addIcons(newIcons) {
|
|
|
200
205
|
export function setupFontAwesome(app) {
|
|
201
206
|
library.add(...Object.values(Icons))
|
|
202
207
|
app.component('FontAwesomeIcon', FontAwesomeIcon)
|
|
208
|
+
|
|
209
|
+
// Auto-register tooltip directive (used by SparkEntityBadge and available to all consumers)
|
|
210
|
+
setupTooltip(app)
|
|
203
211
|
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Icons, addIcons, setupFontAwesome, formKitIcons, formKitIconLoader, formKitGenesisOverride } from './fontawesome.js'
|
|
2
2
|
export { createAuthRoutes, setupAuthGuards, create403Route, create404Route, setupBootstrapGuard, setupDirtyFormGuard } from './router.js'
|
|
3
3
|
export { createAxiosInstance, setupAxios, getAxiosInstance } from './axios.js'
|
|
4
|
-
export { createBootstrapService } from './app-bootstrap.js'
|
|
4
|
+
export { createBootstrapService } from './app-bootstrap.js'
|
|
5
|
+
export { setupTooltip } from './tooltip.js'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { sparkTooltipDirective } from '../directives/sparkTooltip.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the v-spark-tooltip directive globally.
|
|
5
|
+
* Call in your app's main.js: setupTooltip(app)
|
|
6
|
+
*
|
|
7
|
+
* @param {import('vue').App} app - Vue app instance
|
|
8
|
+
*/
|
|
9
|
+
export function setupTooltip(app) {
|
|
10
|
+
app.directive('spark-tooltip', sparkTooltipDirective)
|
|
11
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spark Image Renderer
|
|
3
3
|
*
|
|
4
|
-
* Renders image thumbnails
|
|
4
|
+
* Renders image thumbnails with optional Fancybox lightbox support.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* {
|
|
8
8
|
* data: 'avatar',
|
|
9
9
|
* renderer: 'spark.image',
|
|
10
10
|
* rendererConfig: {
|
|
11
|
-
* size: 'md',
|
|
12
|
-
* rounded: true,
|
|
13
|
-
* alt: 'User avatar' // Alt text (or use row property name)
|
|
11
|
+
* size: 'md', // 'sm' | 'md' | 'lg'
|
|
12
|
+
* rounded: true, // true for circular, false for square
|
|
13
|
+
* alt: 'User avatar', // Alt text (or use row property name)
|
|
14
|
+
* lightbox: true, // Enable click-to-enlarge via Fancybox (requires SparkFancybox wrapper)
|
|
15
|
+
* group: 'gallery', // Fancybox gallery group name (default: 'table-images')
|
|
14
16
|
* }
|
|
15
17
|
* }
|
|
16
18
|
*/
|
|
@@ -37,6 +39,8 @@ export const imageRenderer = (sparkTable) => {
|
|
|
37
39
|
const size = config.size || 'md'
|
|
38
40
|
const sizeClass = sizes[size] || sizes.md
|
|
39
41
|
const rounded = config.rounded !== false // Default to true
|
|
42
|
+
const lightbox = config.lightbox || false
|
|
43
|
+
const group = config.group || 'table-images'
|
|
40
44
|
|
|
41
45
|
// Determine alt text
|
|
42
46
|
let alt = config.alt || ''
|
|
@@ -56,6 +60,17 @@ export const imageRenderer = (sparkTable) => {
|
|
|
56
60
|
img.classList.add('rounded')
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
if (lightbox) {
|
|
64
|
+
// Wrap in <a> with data-fancybox for lightbox support
|
|
65
|
+
const link = document.createElement('a')
|
|
66
|
+
link.href = value
|
|
67
|
+
link.setAttribute('data-fancybox', group)
|
|
68
|
+
if (alt) link.setAttribute('data-caption', alt)
|
|
69
|
+
img.classList.add('cursor-pointer', 'hover:opacity-80', 'transition-opacity')
|
|
70
|
+
link.appendChild(img)
|
|
71
|
+
td.appendChild(link)
|
|
72
|
+
} else {
|
|
73
|
+
td.appendChild(img)
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
}
|