@wishbone-media/spark 0.19.0 → 0.21.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 +1874 -1244
- package/package.json +1 -1
- package/src/assets/css/index.css +1 -0
- package/src/assets/css/nprogress.css +6 -0
- package/src/components/SparkAlert.vue +5 -1
- package/src/components/SparkButton.vue +2 -0
- package/src/components/SparkNotificationOutlet.vue +68 -0
- package/src/components/SparkOverlay.vue +15 -2
- package/src/components/SparkToastContainer.vue +184 -0
- package/src/components/index.js +2 -0
- package/src/composables/index.js +1 -0
- package/src/composables/sparkModalService.js +49 -0
- package/src/composables/sparkNotificationService.js +459 -0
- package/src/composables/sparkOverlayService.js +4 -4
- package/src/composables/useSparkOverlay.js +6 -3
- package/src/composables/useSparkTableRouteSync.js +19 -2
- package/src/utils/sparkTable/renderers/actions.js +27 -2
- package/src/utils/sparkTable/renderers/boolean.js +97 -0
- package/src/utils/sparkTable/renderers/index.js +3 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { markRaw, reactive } from 'vue'
|
|
2
|
+
|
|
3
|
+
// Default durations by type (in ms)
|
|
4
|
+
const DEFAULT_DURATIONS = {
|
|
5
|
+
success: 5000,
|
|
6
|
+
info: 5000,
|
|
7
|
+
warning: 5000,
|
|
8
|
+
danger: 0, // sticky by default
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Valid toast positions
|
|
12
|
+
const TOAST_POSITIONS = ['top-left', 'top-right', 'center', 'bottom-left', 'bottom-right']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a notification outlet state manager (for banner notifications)
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
function createNotificationOutlet() {
|
|
19
|
+
const state = reactive({
|
|
20
|
+
isVisible: false,
|
|
21
|
+
type: 'info',
|
|
22
|
+
message: null,
|
|
23
|
+
component: null,
|
|
24
|
+
props: {},
|
|
25
|
+
closeable: true,
|
|
26
|
+
duration: null,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Timer state (not reactive, internal only)
|
|
30
|
+
let dismissTimeout = null
|
|
31
|
+
let remainingTime = 0
|
|
32
|
+
let isPaused = false
|
|
33
|
+
let timerStartedAt = null
|
|
34
|
+
|
|
35
|
+
const clearDismissTimeout = () => {
|
|
36
|
+
if (dismissTimeout) {
|
|
37
|
+
clearTimeout(dismissTimeout)
|
|
38
|
+
dismissTimeout = null
|
|
39
|
+
}
|
|
40
|
+
remainingTime = 0
|
|
41
|
+
isPaused = false
|
|
42
|
+
timerStartedAt = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const hide = () => {
|
|
46
|
+
clearDismissTimeout()
|
|
47
|
+
state.isVisible = false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const startDismissTimer = (duration) => {
|
|
51
|
+
if (duration <= 0) return
|
|
52
|
+
|
|
53
|
+
remainingTime = duration
|
|
54
|
+
timerStartedAt = Date.now()
|
|
55
|
+
|
|
56
|
+
dismissTimeout = setTimeout(() => {
|
|
57
|
+
hide()
|
|
58
|
+
}, duration)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pause = () => {
|
|
62
|
+
if (!dismissTimeout || isPaused) return
|
|
63
|
+
|
|
64
|
+
clearTimeout(dismissTimeout)
|
|
65
|
+
dismissTimeout = null
|
|
66
|
+
|
|
67
|
+
const elapsed = Date.now() - timerStartedAt
|
|
68
|
+
remainingTime = Math.max(0, remainingTime - elapsed)
|
|
69
|
+
isPaused = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resume = () => {
|
|
73
|
+
if (!isPaused || remainingTime <= 0) return
|
|
74
|
+
|
|
75
|
+
isPaused = false
|
|
76
|
+
timerStartedAt = Date.now()
|
|
77
|
+
|
|
78
|
+
dismissTimeout = setTimeout(() => {
|
|
79
|
+
hide()
|
|
80
|
+
}, remainingTime)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const show = (options = {}) => {
|
|
84
|
+
clearDismissTimeout()
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
type = 'info',
|
|
88
|
+
message = null,
|
|
89
|
+
component = null,
|
|
90
|
+
props = {},
|
|
91
|
+
closeable = true,
|
|
92
|
+
duration = null,
|
|
93
|
+
} = options
|
|
94
|
+
|
|
95
|
+
state.type = type
|
|
96
|
+
state.message = message
|
|
97
|
+
state.component = component ? markRaw(component) : null
|
|
98
|
+
state.props = props
|
|
99
|
+
state.closeable = closeable
|
|
100
|
+
state.isVisible = true
|
|
101
|
+
|
|
102
|
+
// Determine duration: explicit > default for type
|
|
103
|
+
const effectiveDuration = duration !== null ? duration : DEFAULT_DURATIONS[type]
|
|
104
|
+
state.duration = effectiveDuration
|
|
105
|
+
|
|
106
|
+
if (effectiveDuration > 0) {
|
|
107
|
+
startDismissTimer(effectiveDuration)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const reset = () => {
|
|
112
|
+
hide()
|
|
113
|
+
state.type = 'info'
|
|
114
|
+
state.message = null
|
|
115
|
+
state.component = null
|
|
116
|
+
state.props = {}
|
|
117
|
+
state.closeable = true
|
|
118
|
+
state.duration = null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
state,
|
|
123
|
+
show,
|
|
124
|
+
hide,
|
|
125
|
+
reset,
|
|
126
|
+
pause,
|
|
127
|
+
resume,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Service for managing notifications (both banner and toast)
|
|
133
|
+
*
|
|
134
|
+
* Banner Usage:
|
|
135
|
+
* sparkNotificationService.show({ type: 'success', message: 'Saved!' })
|
|
136
|
+
* sparkNotificationService.show({ type: 'error', message: 'Failed' }, 'form-errors')
|
|
137
|
+
* sparkNotificationService.hide()
|
|
138
|
+
*
|
|
139
|
+
* Toast Usage:
|
|
140
|
+
* sparkNotificationService.toast({ type: 'success', message: 'Saved!' })
|
|
141
|
+
* sparkNotificationService.toast({ type: 'error', message: 'Failed', position: 'top-right' })
|
|
142
|
+
* sparkNotificationService.hideToast(toastId)
|
|
143
|
+
* sparkNotificationService.hideAllToasts()
|
|
144
|
+
*/
|
|
145
|
+
class SparkNotificationService {
|
|
146
|
+
constructor() {
|
|
147
|
+
// Banner outlets
|
|
148
|
+
this.outlets = new Map()
|
|
149
|
+
|
|
150
|
+
// Toast state
|
|
151
|
+
this.toastState = reactive({
|
|
152
|
+
toasts: [],
|
|
153
|
+
maxToasts: 3,
|
|
154
|
+
defaultPosition: 'bottom-right',
|
|
155
|
+
newestOnTop: true,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Toast timers (keyed by toast id)
|
|
159
|
+
this._toastTimers = new Map()
|
|
160
|
+
|
|
161
|
+
// Counter for unique toast IDs
|
|
162
|
+
this._toastIdCounter = 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================
|
|
166
|
+
// Banner Notification Methods
|
|
167
|
+
// ============================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get or create a notification outlet by name
|
|
171
|
+
* @param {string} name - Outlet name (default: 'default')
|
|
172
|
+
* @returns {Object} Notification outlet instance
|
|
173
|
+
*/
|
|
174
|
+
getOutlet(name = 'default') {
|
|
175
|
+
if (!this.outlets.has(name)) {
|
|
176
|
+
this.outlets.set(name, createNotificationOutlet())
|
|
177
|
+
}
|
|
178
|
+
return this.outlets.get(name)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Show a banner notification in the specified outlet
|
|
183
|
+
* @param {Object} options - Notification options
|
|
184
|
+
* @param {string} options.type - Alert type: 'success' | 'warning' | 'danger' | 'info'
|
|
185
|
+
* @param {string} options.message - Simple text message (mutually exclusive with component)
|
|
186
|
+
* @param {Component} options.component - Vue component to render (mutually exclusive with message)
|
|
187
|
+
* @param {Object} options.props - Props to pass to the component
|
|
188
|
+
* @param {boolean} options.closeable - Whether to show close button (default: true)
|
|
189
|
+
* @param {number} options.duration - Auto-dismiss duration in ms (0 = sticky, null = use default)
|
|
190
|
+
* @param {string} outletName - Target outlet name (default: 'default')
|
|
191
|
+
*/
|
|
192
|
+
show(options = {}, outletName = 'default') {
|
|
193
|
+
const outlet = this.getOutlet(outletName)
|
|
194
|
+
outlet.show(options)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Hide banner notification in the specified outlet
|
|
199
|
+
* @param {string} outletName - Target outlet name (default: 'default')
|
|
200
|
+
*/
|
|
201
|
+
hide(outletName = 'default') {
|
|
202
|
+
const outlet = this.getOutlet(outletName)
|
|
203
|
+
outlet.hide()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Hide all banner notifications in all outlets
|
|
208
|
+
*/
|
|
209
|
+
hideAll() {
|
|
210
|
+
for (const outlet of this.outlets.values()) {
|
|
211
|
+
outlet.hide()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Pause auto-dismiss timer for the specified banner outlet
|
|
217
|
+
* @param {string} outletName - Target outlet name (default: 'default')
|
|
218
|
+
*/
|
|
219
|
+
pause(outletName = 'default') {
|
|
220
|
+
const outlet = this.getOutlet(outletName)
|
|
221
|
+
outlet.pause()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resume auto-dismiss timer for the specified banner outlet
|
|
226
|
+
* @param {string} outletName - Target outlet name (default: 'default')
|
|
227
|
+
*/
|
|
228
|
+
resume(outletName = 'default') {
|
|
229
|
+
const outlet = this.getOutlet(outletName)
|
|
230
|
+
outlet.resume()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reset and remove a banner outlet
|
|
235
|
+
* @param {string} outletName - Target outlet name (default: 'default')
|
|
236
|
+
*/
|
|
237
|
+
reset(outletName = 'default') {
|
|
238
|
+
const outlet = this.outlets.get(outletName)
|
|
239
|
+
if (outlet) {
|
|
240
|
+
outlet.reset()
|
|
241
|
+
this.outlets.delete(outletName)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Reset all banner outlets
|
|
247
|
+
*/
|
|
248
|
+
resetAll() {
|
|
249
|
+
for (const outlet of this.outlets.values()) {
|
|
250
|
+
outlet.reset()
|
|
251
|
+
}
|
|
252
|
+
this.outlets.clear()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================
|
|
256
|
+
// Toast Notification Methods
|
|
257
|
+
// ============================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Configure toast defaults
|
|
261
|
+
* @param {Object} options - Configuration options
|
|
262
|
+
* @param {number} options.maxToasts - Maximum visible toasts (default: 3)
|
|
263
|
+
* @param {string} options.defaultPosition - Default position (default: 'bottom-right')
|
|
264
|
+
* @param {boolean} options.newestOnTop - Whether newest toasts appear on top (default: true)
|
|
265
|
+
*/
|
|
266
|
+
configureToasts(options = {}) {
|
|
267
|
+
if (options.maxToasts !== undefined) {
|
|
268
|
+
this.toastState.maxToasts = options.maxToasts
|
|
269
|
+
}
|
|
270
|
+
if (options.defaultPosition !== undefined && TOAST_POSITIONS.includes(options.defaultPosition)) {
|
|
271
|
+
this.toastState.defaultPosition = options.defaultPosition
|
|
272
|
+
}
|
|
273
|
+
if (options.newestOnTop !== undefined) {
|
|
274
|
+
this.toastState.newestOnTop = options.newestOnTop
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Show a toast notification
|
|
280
|
+
* @param {Object} options - Toast options
|
|
281
|
+
* @param {string} options.type - Alert type: 'success' | 'warning' | 'danger' | 'info'
|
|
282
|
+
* @param {string} options.message - Simple text message (mutually exclusive with component)
|
|
283
|
+
* @param {Component} options.component - Vue component to render (mutually exclusive with message)
|
|
284
|
+
* @param {Object} options.props - Props to pass to the component
|
|
285
|
+
* @param {boolean} options.closeable - Whether to show close button (default: true)
|
|
286
|
+
* @param {number} options.duration - Auto-dismiss duration in ms (0 = sticky, null = use default)
|
|
287
|
+
* @param {string} options.position - Position: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center'
|
|
288
|
+
* @returns {number} Toast ID for programmatic dismissal
|
|
289
|
+
*/
|
|
290
|
+
toast(options = {}) {
|
|
291
|
+
const {
|
|
292
|
+
type = 'info',
|
|
293
|
+
message = null,
|
|
294
|
+
component = null,
|
|
295
|
+
props = {},
|
|
296
|
+
closeable = true,
|
|
297
|
+
duration = null,
|
|
298
|
+
position = null,
|
|
299
|
+
} = options
|
|
300
|
+
|
|
301
|
+
// Generate unique ID
|
|
302
|
+
const id = ++this._toastIdCounter
|
|
303
|
+
|
|
304
|
+
// Determine effective values
|
|
305
|
+
const effectivePosition = (position && TOAST_POSITIONS.includes(position))
|
|
306
|
+
? position
|
|
307
|
+
: this.toastState.defaultPosition
|
|
308
|
+
const effectiveDuration = duration !== null ? duration : DEFAULT_DURATIONS[type]
|
|
309
|
+
|
|
310
|
+
// Create toast object
|
|
311
|
+
const toast = {
|
|
312
|
+
id,
|
|
313
|
+
type,
|
|
314
|
+
message,
|
|
315
|
+
component: component ? markRaw(component) : null,
|
|
316
|
+
props,
|
|
317
|
+
closeable,
|
|
318
|
+
duration: effectiveDuration,
|
|
319
|
+
position: effectivePosition,
|
|
320
|
+
createdAt: Date.now(),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Remove oldest toasts if at capacity (per position)
|
|
324
|
+
const positionToasts = this.toastState.toasts.filter(t => t.position === effectivePosition)
|
|
325
|
+
if (positionToasts.length >= this.toastState.maxToasts) {
|
|
326
|
+
// Remove oldest: when newestOnTop, oldest is at end; otherwise at beginning
|
|
327
|
+
const oldest = this.toastState.newestOnTop
|
|
328
|
+
? positionToasts[positionToasts.length - 1]
|
|
329
|
+
: positionToasts[0]
|
|
330
|
+
this.hideToast(oldest.id)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add toast to array
|
|
334
|
+
if (this.toastState.newestOnTop) {
|
|
335
|
+
// Find insertion point: after all toasts of same position
|
|
336
|
+
const insertIndex = this.toastState.toasts.findIndex(t => t.position === effectivePosition)
|
|
337
|
+
if (insertIndex === -1) {
|
|
338
|
+
this.toastState.toasts.push(toast)
|
|
339
|
+
} else {
|
|
340
|
+
this.toastState.toasts.splice(insertIndex, 0, toast)
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
this.toastState.toasts.push(toast)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Start auto-dismiss timer if duration > 0
|
|
347
|
+
if (effectiveDuration > 0) {
|
|
348
|
+
this._startToastTimer(id, effectiveDuration)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return id
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Hide a specific toast by ID
|
|
356
|
+
* @param {number} toastId - Toast ID to hide
|
|
357
|
+
*/
|
|
358
|
+
hideToast(toastId) {
|
|
359
|
+
// Clear timer
|
|
360
|
+
this._clearToastTimer(toastId)
|
|
361
|
+
|
|
362
|
+
// Remove from array
|
|
363
|
+
const index = this.toastState.toasts.findIndex(t => t.id === toastId)
|
|
364
|
+
if (index !== -1) {
|
|
365
|
+
this.toastState.toasts.splice(index, 1)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Hide all toasts
|
|
371
|
+
*/
|
|
372
|
+
hideAllToasts() {
|
|
373
|
+
// Clear all timers
|
|
374
|
+
for (const id of this._toastTimers.keys()) {
|
|
375
|
+
this._clearToastTimer(id)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clear array
|
|
379
|
+
this.toastState.toasts.splice(0, this.toastState.toasts.length)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Pause auto-dismiss timer for a specific toast
|
|
384
|
+
* @param {number} toastId - Toast ID
|
|
385
|
+
*/
|
|
386
|
+
pauseToast(toastId) {
|
|
387
|
+
const timer = this._toastTimers.get(toastId)
|
|
388
|
+
if (!timer || timer.isPaused) return
|
|
389
|
+
|
|
390
|
+
clearTimeout(timer.timeout)
|
|
391
|
+
timer.timeout = null
|
|
392
|
+
|
|
393
|
+
const elapsed = Date.now() - timer.startedAt
|
|
394
|
+
timer.remainingTime = Math.max(0, timer.remainingTime - elapsed)
|
|
395
|
+
timer.isPaused = true
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resume auto-dismiss timer for a specific toast
|
|
400
|
+
* @param {number} toastId - Toast ID
|
|
401
|
+
*/
|
|
402
|
+
resumeToast(toastId) {
|
|
403
|
+
const timer = this._toastTimers.get(toastId)
|
|
404
|
+
if (!timer || !timer.isPaused || timer.remainingTime <= 0) return
|
|
405
|
+
|
|
406
|
+
timer.isPaused = false
|
|
407
|
+
timer.startedAt = Date.now()
|
|
408
|
+
|
|
409
|
+
timer.timeout = setTimeout(() => {
|
|
410
|
+
this.hideToast(toastId)
|
|
411
|
+
}, timer.remainingTime)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get toasts for a specific position
|
|
416
|
+
* @param {string} position - Position to filter by
|
|
417
|
+
* @returns {Array} Toasts at that position
|
|
418
|
+
*/
|
|
419
|
+
getToastsByPosition(position) {
|
|
420
|
+
return this.toastState.toasts.filter(t => t.position === position)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================
|
|
424
|
+
// Private Toast Timer Methods
|
|
425
|
+
// ============================================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
_startToastTimer(toastId, duration) {
|
|
431
|
+
const timer = {
|
|
432
|
+
timeout: null,
|
|
433
|
+
remainingTime: duration,
|
|
434
|
+
startedAt: Date.now(),
|
|
435
|
+
isPaused: false,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
timer.timeout = setTimeout(() => {
|
|
439
|
+
this.hideToast(toastId)
|
|
440
|
+
}, duration)
|
|
441
|
+
|
|
442
|
+
this._toastTimers.set(toastId, timer)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_clearToastTimer(toastId) {
|
|
449
|
+
const timer = this._toastTimers.get(toastId)
|
|
450
|
+
if (timer) {
|
|
451
|
+
if (timer.timeout) {
|
|
452
|
+
clearTimeout(timer.timeout)
|
|
453
|
+
}
|
|
454
|
+
this._toastTimers.delete(toastId)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export const sparkNotificationService = new SparkNotificationService()
|
|
@@ -6,7 +6,7 @@ class SparkOverlayService {
|
|
|
6
6
|
this.right = useSparkOverlay()
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
showLeft = (component, props = {}, eventHandlers = {}) => {
|
|
9
|
+
showLeft = (component, props = {}, eventHandlers = {}, options = {}) => {
|
|
10
10
|
const handlers = {
|
|
11
11
|
close: () => {
|
|
12
12
|
// Call user's close handler if provided
|
|
@@ -16,10 +16,10 @@ class SparkOverlayService {
|
|
|
16
16
|
},
|
|
17
17
|
...eventHandlers,
|
|
18
18
|
}
|
|
19
|
-
this.left.show(component, props, handlers)
|
|
19
|
+
this.left.show(component, props, handlers, options)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
showRight = (component, props = {}, eventHandlers = {}) => {
|
|
22
|
+
showRight = (component, props = {}, eventHandlers = {}, options = {}) => {
|
|
23
23
|
const handlers = {
|
|
24
24
|
close: () => {
|
|
25
25
|
// Call user's close handler if provided
|
|
@@ -29,7 +29,7 @@ class SparkOverlayService {
|
|
|
29
29
|
},
|
|
30
30
|
...eventHandlers,
|
|
31
31
|
}
|
|
32
|
-
this.right.show(component, props, handlers)
|
|
32
|
+
this.right.show(component, props, handlers, options)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
closeLeft = () => {
|
|
@@ -6,6 +6,7 @@ export function useSparkOverlay() {
|
|
|
6
6
|
content: null,
|
|
7
7
|
props: {},
|
|
8
8
|
eventHandlers: {},
|
|
9
|
+
size: 'md',
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
const toggle = () => {
|
|
@@ -17,20 +18,22 @@ export function useSparkOverlay() {
|
|
|
17
18
|
state.content = null
|
|
18
19
|
state.props = {}
|
|
19
20
|
state.eventHandlers = {}
|
|
21
|
+
state.size = 'md'
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const open = () => {
|
|
23
25
|
state.isVisible = true
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const setContent = (content, props = {}, eventHandlers = {}) => {
|
|
28
|
+
const setContent = (content, props = {}, eventHandlers = {}, options = {}) => {
|
|
27
29
|
state.content = markRaw(content)
|
|
28
30
|
state.props = props
|
|
29
31
|
state.eventHandlers = eventHandlers
|
|
32
|
+
state.size = options.size || 'md'
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
const show = (content, props = {}, eventHandlers = {}) => {
|
|
33
|
-
if (content) setContent(content, props, eventHandlers)
|
|
35
|
+
const show = (content, props = {}, eventHandlers = {}, options = {}) => {
|
|
36
|
+
if (content) setContent(content, props, eventHandlers, options)
|
|
34
37
|
open()
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -203,6 +203,19 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
|
|
|
203
203
|
return false
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Coerce URL query param values to their proper types
|
|
208
|
+
* URL params are always strings, but page/limit should be integers
|
|
209
|
+
*/
|
|
210
|
+
const coerceParamValue = (key, value) => {
|
|
211
|
+
const numericParams = ['page', 'limit']
|
|
212
|
+
if (numericParams.includes(key) && value !== null && value !== undefined) {
|
|
213
|
+
const parsed = parseInt(value, 10)
|
|
214
|
+
return isNaN(parsed) ? value : parsed
|
|
215
|
+
}
|
|
216
|
+
return value
|
|
217
|
+
}
|
|
218
|
+
|
|
206
219
|
/**
|
|
207
220
|
* Restore params from route query to sparkTable
|
|
208
221
|
*/
|
|
@@ -211,7 +224,7 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
|
|
|
211
224
|
// Flat mode: read any query param that looks like a table param
|
|
212
225
|
Object.keys(route.query).forEach((key) => {
|
|
213
226
|
if (isTableParam(key)) {
|
|
214
|
-
sparkTable.params[key] = route.query[key]
|
|
227
|
+
sparkTable.params[key] = coerceParamValue(key, route.query[key])
|
|
215
228
|
}
|
|
216
229
|
})
|
|
217
230
|
} else {
|
|
@@ -224,7 +237,7 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
|
|
|
224
237
|
// e.g., "table[selectFilters[is_quote]]" => "selectFilters[is_quote]"
|
|
225
238
|
// e.g., "table[page]" => "page"
|
|
226
239
|
const paramKey = key.slice(prefix.length, -1)
|
|
227
|
-
sparkTable.params[paramKey] = route.query[key]
|
|
240
|
+
sparkTable.params[paramKey] = coerceParamValue(paramKey, route.query[key])
|
|
228
241
|
}
|
|
229
242
|
})
|
|
230
243
|
}
|
|
@@ -275,6 +288,10 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
|
|
|
275
288
|
if (!hasUrlParams && persistToStorage) {
|
|
276
289
|
const storedParams = loadFromStorage()
|
|
277
290
|
if (storedParams && Object.keys(storedParams).length > 0) {
|
|
291
|
+
// Coerce numeric params in case localStorage has string values from previous buggy saves
|
|
292
|
+
Object.keys(storedParams).forEach((key) => {
|
|
293
|
+
storedParams[key] = coerceParamValue(key, storedParams[key])
|
|
294
|
+
})
|
|
278
295
|
Object.assign(sparkTable.params, storedParams)
|
|
279
296
|
}
|
|
280
297
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { icon } from '@fortawesome/fontawesome-svg-core'
|
|
2
|
+
import { sparkModalService } from '@/composables/sparkModalService'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Spark Actions Renderer
|
|
@@ -25,6 +26,16 @@ import { icon } from '@fortawesome/fontawesome-svg-core'
|
|
|
25
26
|
* {
|
|
26
27
|
* icon: 'download',
|
|
27
28
|
* handler: (row) => downloadFile(row.file_url)
|
|
29
|
+
* },
|
|
30
|
+
* {
|
|
31
|
+
* icon: 'trash',
|
|
32
|
+
* label: 'Delete',
|
|
33
|
+
* event: 'deleteRow',
|
|
34
|
+
* confirm: true,
|
|
35
|
+
* confirmTitle: 'Delete Item',
|
|
36
|
+
* confirmType: 'danger',
|
|
37
|
+
* confirmText: 'Delete',
|
|
38
|
+
* cancelText: 'Keep'
|
|
28
39
|
* }
|
|
29
40
|
* ]
|
|
30
41
|
* }
|
|
@@ -37,6 +48,11 @@ import { icon } from '@fortawesome/fontawesome-svg-core'
|
|
|
37
48
|
* @property {string} [event] - Event name to emit (e.g., 'editRow')
|
|
38
49
|
* @property {Function} [handler] - Custom handler function (row) => void
|
|
39
50
|
* @property {boolean|string} [confirm] - Show confirmation dialog (true or custom message)
|
|
51
|
+
* @property {string} [confirmTitle] - Custom confirmation dialog title (default: 'Confirm')
|
|
52
|
+
* @property {string} [confirmType] - Confirmation dialog type: 'info', 'success', 'warning', 'danger' (default: 'warning')
|
|
53
|
+
* @property {string} [confirmText] - Confirm button text (default: 'Confirm')
|
|
54
|
+
* @property {string} [cancelText] - Cancel button text (default: 'Cancel')
|
|
55
|
+
* @property {string} [confirmVariant] - Confirm button variant (default: 'primary')
|
|
40
56
|
* @property {Function} [condition] - Conditional visibility (row) => boolean
|
|
41
57
|
*/
|
|
42
58
|
|
|
@@ -89,7 +105,7 @@ export const actionsRenderer = (sparkTable) => {
|
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
// Add click handler
|
|
92
|
-
button.addEventListener('click', (e) => {
|
|
108
|
+
button.addEventListener('click', async (e) => {
|
|
93
109
|
e.preventDefault()
|
|
94
110
|
e.stopPropagation()
|
|
95
111
|
|
|
@@ -100,7 +116,16 @@ export const actionsRenderer = (sparkTable) => {
|
|
|
100
116
|
? action.confirm
|
|
101
117
|
: `Are you sure you want to ${action.label?.toLowerCase() || 'perform this action'}?`
|
|
102
118
|
|
|
103
|
-
|
|
119
|
+
const confirmed = await sparkModalService.confirm({
|
|
120
|
+
title: action.confirmTitle,
|
|
121
|
+
message: confirmMessage,
|
|
122
|
+
type: action.confirmType,
|
|
123
|
+
confirmText: action.confirmText,
|
|
124
|
+
cancelText: action.cancelText,
|
|
125
|
+
confirmVariant: action.confirmVariant,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!confirmed) {
|
|
104
129
|
return
|
|
105
130
|
}
|
|
106
131
|
}
|