@wishbone-media/spark 0.35.0 → 0.37.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.css +1 -1
- package/dist/index.js +4025 -2544
- package/package.json +2 -1
- package/src/assets/css/spark-address-input.css +67 -0
- package/src/assets/css/spark-tooltip.css +44 -0
- package/src/components/SparkAddressInput.vue +2 -14
- package/src/components/SparkEntityBadge.vue +175 -0
- package/src/components/SparkTooltip.vue +162 -0
- package/src/components/index.js +2 -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
|
@@ -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
|
+
}
|