@bagelink/vue 1.15.482 → 1.15.486
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/core.css +1 -1
- package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +6 -0
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/index.cjs +75 -75
- package/dist/index.css +1 -1
- package/dist/index.mjs +8592 -8513
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/form-flow/FormFlow.vue +1 -0
- package/src/form-flow/FormFlowPlayer.vue +148 -4
package/package.json
CHANGED
|
@@ -400,6 +400,7 @@ function getFieldProps(field: FieldBuilder, key: string) {
|
|
|
400
400
|
'onUpdate:modelValue': (val: any) => { updateField(key, val, field) },
|
|
401
401
|
'error': mergedErrors.value[key],
|
|
402
402
|
...configRest,
|
|
403
|
+
'formData': formData.value,
|
|
403
404
|
// Inputs use `helptext`; FormFlow schema API keeps `helpText`
|
|
404
405
|
...(helpText != null ? { helptext: helpText } : {}),
|
|
405
406
|
name: field._config.name ?? key,
|
|
@@ -23,6 +23,9 @@ import { normalizeCustomFormSubmission } from './custom-field-registry'
|
|
|
23
23
|
import { fromJSONSchema } from './form-flow'
|
|
24
24
|
import type { JSONSchemaOptionsSourceLoader } from './form-flow'
|
|
25
25
|
import Btn from '../components/Btn.vue'
|
|
26
|
+
import Icon from '../components/Icon/Icon.vue'
|
|
27
|
+
import ListItem from '../components/ListItem.vue'
|
|
28
|
+
import Progress from '../components/Progress.vue'
|
|
26
29
|
import FormFlow from './FormFlow.vue'
|
|
27
30
|
import type { FormStepCompleteEventPayload, FormStepEventPayload } from './structure'
|
|
28
31
|
import { autoAdvanceFieldKey, entryStepKey, pathProgress, resolveNext, stepAutoAdvances, stepByKey } from './structure'
|
|
@@ -31,6 +34,8 @@ export interface Props {
|
|
|
31
34
|
structure: FormStructure
|
|
32
35
|
modelValue?: Record<string, any>
|
|
33
36
|
components?: Record<string, any>
|
|
37
|
+
/** Stable step key to resume. Falls back to the structure entry step. */
|
|
38
|
+
initialStepKey?: string | null
|
|
34
39
|
/** Human name of the form, included in step/complete event payloads. */
|
|
35
40
|
formName?: string
|
|
36
41
|
/** Slide between steps (honors prefers-reduced-motion). Default true. */
|
|
@@ -52,6 +57,8 @@ export interface Props {
|
|
|
52
57
|
hideTitle?: boolean
|
|
53
58
|
/** Hide the step-position counter pill in the default nav. Default false. */
|
|
54
59
|
hideCounter?: boolean
|
|
60
|
+
/** Show numbered stages with completed-step navigation. Default false. */
|
|
61
|
+
showStages?: boolean
|
|
55
62
|
/** Optional async submit handler; awaited before `complete` is emitted. */
|
|
56
63
|
onComplete?: (data: Record<string, any>) => Promise<void> | void
|
|
57
64
|
/**
|
|
@@ -75,6 +82,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
75
82
|
redirect: true,
|
|
76
83
|
hideTitle: false,
|
|
77
84
|
hideCounter: false,
|
|
85
|
+
showStages: false,
|
|
78
86
|
})
|
|
79
87
|
|
|
80
88
|
const emit = defineEmits<{
|
|
@@ -116,14 +124,30 @@ watch(() => props.modelValue, (val) => {
|
|
|
116
124
|
|
|
117
125
|
// Navigation state: the current step key plus the stack of previously visited
|
|
118
126
|
// keys (for Back). Initialised to the structure's entry step.
|
|
119
|
-
|
|
127
|
+
function initialKey(): string | undefined {
|
|
128
|
+
const requested = props.initialStepKey
|
|
129
|
+
return requested && stepByKey(props.structure, requested)
|
|
130
|
+
? requested
|
|
131
|
+
: entryStepKey(props.structure)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const currentKey = ref<string | undefined>(initialKey())
|
|
120
135
|
const history = ref<string[]>([])
|
|
136
|
+
const completedKeys = ref<Set<string>>(new Set())
|
|
121
137
|
|
|
122
138
|
watch(() => props.structure, () => {
|
|
123
|
-
currentKey.value =
|
|
139
|
+
currentKey.value = initialKey()
|
|
124
140
|
history.value = []
|
|
141
|
+
completedKeys.value = new Set()
|
|
125
142
|
}, { deep: true })
|
|
126
143
|
|
|
144
|
+
watch(() => props.initialStepKey, (key) => {
|
|
145
|
+
if (!key || key === currentKey.value || !stepByKey(props.structure, key)) return
|
|
146
|
+
currentKey.value = key
|
|
147
|
+
history.value = []
|
|
148
|
+
completedKeys.value = new Set()
|
|
149
|
+
})
|
|
150
|
+
|
|
127
151
|
watch(currentKey, (key) => {
|
|
128
152
|
if (key) emit('stepChange', key)
|
|
129
153
|
}, { immediate: true })
|
|
@@ -188,6 +212,32 @@ const canGoBack = computed(() => history.value.length > 0)
|
|
|
188
212
|
// host can render its own <Progress> (the player stays chrome-light).
|
|
189
213
|
const progress = computed(() => pathProgress(props.structure, currentKey.value, formData.value))
|
|
190
214
|
watch(progress, val => emit('progress', val), { immediate: true })
|
|
215
|
+
const pathSteps = computed(() => {
|
|
216
|
+
const result: FormStep[] = []
|
|
217
|
+
const seen = new Set<string>()
|
|
218
|
+
let key = entryStepKey(props.structure)
|
|
219
|
+
while (key && !seen.has(key)) {
|
|
220
|
+
seen.add(key)
|
|
221
|
+
const step = stepByKey(props.structure, key)
|
|
222
|
+
if (!step) break
|
|
223
|
+
result.push(step)
|
|
224
|
+
key = resolveNext(props.structure, key, formData.value) ?? undefined
|
|
225
|
+
}
|
|
226
|
+
return result
|
|
227
|
+
})
|
|
228
|
+
const currentPathIndex = computed(() =>
|
|
229
|
+
pathSteps.value.findIndex(step => step.key === currentKey.value)
|
|
230
|
+
)
|
|
231
|
+
watch([pathSteps, currentPathIndex], ([steps, index]) => {
|
|
232
|
+
if (index <= 0) return
|
|
233
|
+
if (history.value.length === 0) {
|
|
234
|
+
history.value = steps.slice(0, index).map(step => step.key)
|
|
235
|
+
}
|
|
236
|
+
completedKeys.value = new Set([
|
|
237
|
+
...completedKeys.value,
|
|
238
|
+
...steps.slice(0, index).map(step => step.key),
|
|
239
|
+
])
|
|
240
|
+
}, { immediate: true })
|
|
191
241
|
|
|
192
242
|
const formRef = ref<{ validate: () => boolean } | null>(null)
|
|
193
243
|
const submitting = ref(false)
|
|
@@ -243,6 +293,7 @@ async function onPrimary() {
|
|
|
243
293
|
// Advance to the resolved next step.
|
|
244
294
|
const target = nextKey.value
|
|
245
295
|
if (target && currentKey.value) {
|
|
296
|
+
completedKeys.value = new Set([...completedKeys.value, currentKey.value])
|
|
246
297
|
const destination = stepByKey(props.structure, target)
|
|
247
298
|
emit('stepComplete', {
|
|
248
299
|
...buildPayload(currentKey.value),
|
|
@@ -310,15 +361,68 @@ function onBack() {
|
|
|
310
361
|
}
|
|
311
362
|
}
|
|
312
363
|
|
|
364
|
+
function canOpenStage(key: string): boolean {
|
|
365
|
+
if (key === currentKey.value || completedKeys.value.has(key)) return true
|
|
366
|
+
return key === nextKey.value
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function goToStage(key: string) {
|
|
370
|
+
if (key === currentKey.value || !canOpenStage(key)) return
|
|
371
|
+
const targetIndex = pathSteps.value.findIndex(step => step.key === key)
|
|
372
|
+
const currentIndex = currentPathIndex.value
|
|
373
|
+
if (targetIndex < 0 || currentIndex < 0) return
|
|
374
|
+
if (key === nextKey.value) {
|
|
375
|
+
await onPrimary()
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
slideDir.value = targetIndex < currentIndex ? 'prev' : 'next'
|
|
379
|
+
history.value = pathSteps.value.slice(0, targetIndex).map(step => step.key)
|
|
380
|
+
currentKey.value = key
|
|
381
|
+
}
|
|
382
|
+
|
|
313
383
|
// Progress: position in the visited path (best-effort; branches make total
|
|
314
384
|
// step count indeterminate, so we show visited-count rather than a fixed total).
|
|
315
385
|
const stepPosition = computed(() => history.value.length + 1)
|
|
316
386
|
|
|
317
|
-
defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep })
|
|
387
|
+
defineExpose({ formData, currentKey, nextKey, isTerminal, progress, completedKeys, validateStep })
|
|
318
388
|
</script>
|
|
319
389
|
|
|
320
390
|
<template>
|
|
321
|
-
<div class="form-flow-player">
|
|
391
|
+
<div class="form-flow-player" :class="{ 'form-flow-player--stages': showStages }">
|
|
392
|
+
<nav v-if="showStages" class="ffp-stages" aria-label="Form stages">
|
|
393
|
+
<ol class="ffp-stage-list p-0 m-0">
|
|
394
|
+
<li v-for="(step, index) in pathSteps" :key="step.key">
|
|
395
|
+
<ListItem
|
|
396
|
+
rounded
|
|
397
|
+
color="purple"
|
|
398
|
+
:title="step.title"
|
|
399
|
+
:active="currentKey === step.key"
|
|
400
|
+
:disabled="!canOpenStage(step.key)"
|
|
401
|
+
:aria-current="currentKey === step.key ? 'step' : undefined"
|
|
402
|
+
@click="goToStage(step.key)"
|
|
403
|
+
>
|
|
404
|
+
<template #media>
|
|
405
|
+
<span
|
|
406
|
+
class="ffp-stage-number flex-center round border"
|
|
407
|
+
:class="currentKey === step.key
|
|
408
|
+
? 'bg-primary color-white border-transparent'
|
|
409
|
+
: completedKeys.has(step.key)
|
|
410
|
+
? 'bg-green color-white border-green'
|
|
411
|
+
: 'color-gray'"
|
|
412
|
+
>
|
|
413
|
+
<Icon v-if="completedKeys.has(step.key) && currentKey !== step.key" icon="check" :size="0.9" />
|
|
414
|
+
<template v-else>{{ index + 1 }}</template>
|
|
415
|
+
</span>
|
|
416
|
+
</template>
|
|
417
|
+
</ListItem>
|
|
418
|
+
</li>
|
|
419
|
+
</ol>
|
|
420
|
+
<div class="ffp-stage-progress">
|
|
421
|
+
<span>{{ currentStep?.title }}</span>
|
|
422
|
+
<span>{{ currentPathIndex + 1 }} / {{ pathSteps.length }}</span>
|
|
423
|
+
<Progress class="w-100p mt-05" :value="progress" :size="8" />
|
|
424
|
+
</div>
|
|
425
|
+
</nav>
|
|
322
426
|
<div class="ffp-stage">
|
|
323
427
|
<Transition :name="transitionName" mode="out-in">
|
|
324
428
|
<div v-if="currentStep" :key="currentKey" class="ffp-step">
|
|
@@ -425,6 +529,29 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
|
|
|
425
529
|
gap: 1.5rem;
|
|
426
530
|
}
|
|
427
531
|
|
|
532
|
+
.form-flow-player--stages {
|
|
533
|
+
display: grid;
|
|
534
|
+
grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.ffp-stage-list {
|
|
538
|
+
list-style: none;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.ffp-stage-number {
|
|
542
|
+
width: 2rem;
|
|
543
|
+
height: 2rem;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.ffp-stage-progress {
|
|
547
|
+
display: none;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.form-flow-player--stages > .ffp-stage,
|
|
551
|
+
.form-flow-player--stages > .ffp-nav {
|
|
552
|
+
grid-column: 2;
|
|
553
|
+
}
|
|
554
|
+
|
|
428
555
|
.ffp-title {
|
|
429
556
|
margin: 0 0 1rem 0;
|
|
430
557
|
font-size: 1.5rem;
|
|
@@ -470,4 +597,21 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
|
|
|
470
597
|
transform: none;
|
|
471
598
|
}
|
|
472
599
|
}
|
|
600
|
+
|
|
601
|
+
@media (max-width: 910px) {
|
|
602
|
+
.form-flow-player {
|
|
603
|
+
display: flex;
|
|
604
|
+
flex-direction: column;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.ffp-stage-list {
|
|
608
|
+
display: none;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.ffp-stage-progress {
|
|
612
|
+
display: flex;
|
|
613
|
+
flex-wrap: wrap;
|
|
614
|
+
justify-content: space-between;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
473
617
|
</style>
|