|
| 1 | +<script setup lang="ts"> |
| 2 | +import { ref, computed, watch, provide } from 'vue'; |
| 3 | +import type { ComponentPublicInstance } from 'vue'; |
| 4 | +import type { |
| 5 | + AnyControlNode, |
| 6 | + AnyUnsupportedControlNode, |
| 7 | + GeneralChildNode, |
| 8 | + GroupNode, |
| 9 | + RepeatRangeNode, |
| 10 | +} from '@getodk/xforms-engine'; |
| 11 | +import FormGroup from './FormGroup.vue'; |
| 12 | +import FormQuestion from './FormQuestion.vue'; |
| 13 | +import RepeatRange from './RepeatRange.vue'; |
| 14 | +import ExpectModelNode from './dev-only/ExpectModelNode.vue'; |
| 15 | +import Steps from 'primevue/steps'; |
| 16 | +import ProgressBar from 'primevue/progressbar'; |
| 17 | +import Button from 'primevue/button'; |
| 18 | +
|
| 19 | +const props = defineProps<{ nodes: readonly GeneralChildNode[] }>(); |
| 20 | +const emit = defineEmits(['endOfForm']); |
| 21 | +
|
| 22 | +const isGroupNode = (node: GeneralChildNode): node is GroupNode => { |
| 23 | + return node.nodeType === 'group'; |
| 24 | +}; |
| 25 | +
|
| 26 | +type NonGroupNode = Exclude<GeneralChildNode, GroupNode>; |
| 27 | +
|
| 28 | +const isRepeatRangeNode = (node: NonGroupNode): node is RepeatRangeNode => { |
| 29 | + return ( |
| 30 | + node.nodeType === 'repeat-range:controlled' || node.nodeType === 'repeat-range:uncontrolled' |
| 31 | + ); |
| 32 | +}; |
| 33 | +
|
| 34 | +type NonStructuralNode = Exclude<NonGroupNode, RepeatRangeNode>; |
| 35 | +
|
| 36 | +type ControlNode = AnyControlNode | AnyUnsupportedControlNode; |
| 37 | +
|
| 38 | +const isControlNode = (node: NonStructuralNode): node is ControlNode => { |
| 39 | + const { nodeType } = node; |
| 40 | +
|
| 41 | + return ( |
| 42 | + nodeType === 'input' || |
| 43 | + nodeType === 'note' || |
| 44 | + nodeType === 'select' || |
| 45 | + nodeType === 'trigger' || |
| 46 | + nodeType === 'range' || |
| 47 | + nodeType === 'rank' || |
| 48 | + nodeType === 'upload' |
| 49 | + ); |
| 50 | +}; |
| 51 | +
|
| 52 | +// Compute step items (only groups, repeat ranges, and control nodes should be steps) |
| 53 | +const relevantNodes = computed(() => |
| 54 | + props.nodes.filter(node => node.currentState.relevant) |
| 55 | +); |
| 56 | +const steps = computed(() => |
| 57 | + relevantNodes.value |
| 58 | + .filter(node => isGroupNode(node) || isRepeatRangeNode(node) || isControlNode(node)) |
| 59 | +); |
| 60 | +
|
| 61 | +// Handle stepper state |
| 62 | +const currentStep = ref(0); |
| 63 | +const isCurrentStepValidated = ref(true); |
| 64 | +const submitPressed = ref(false); |
| 65 | +provide('submitPressed', submitPressed); |
| 66 | +
|
| 67 | +const validateStep = () => { |
| 68 | + // Manually trigger submitPressed to display error messages |
| 69 | + submitPressed.value = true; |
| 70 | +
|
| 71 | + const currentNode = steps.value[currentStep.value]; |
| 72 | + if (isGroupNode(currentNode) && currentNode.validationState.violations.length > 0) { |
| 73 | + isCurrentStepValidated.value = false; |
| 74 | + } else if (currentNode.validationState.violation) { |
| 75 | + isCurrentStepValidated.value = false; |
| 76 | + } else { |
| 77 | + isCurrentStepValidated.value = true; |
| 78 | + } |
| 79 | +} |
| 80 | +const nextStep = () => { |
| 81 | + validateStep(); |
| 82 | +
|
| 83 | + if (isCurrentStepValidated.value && currentStep.value < steps.value.length - 1) { |
| 84 | + // Reset validation triggered later in the form |
| 85 | + submitPressed.value = false; |
| 86 | + // Also reset validation state of current node |
| 87 | + isCurrentStepValidated.value = true; |
| 88 | + currentStep.value++; |
| 89 | + } |
| 90 | +}; |
| 91 | +const prevStep = () => { |
| 92 | + if (currentStep.value > 0) { |
| 93 | + currentStep.value--; |
| 94 | + } |
| 95 | +}; |
| 96 | +const isLastStep = computed(() => currentStep.value === steps.value.length - 1); |
| 97 | +watch(isLastStep, (newValue) => { |
| 98 | + emit('endOfForm', newValue); |
| 99 | +}); |
| 100 | +
|
| 101 | +// // Calculate stepper progress |
| 102 | +// const totalNodes = computed(() => |
| 103 | +// Math.max(relevantNodes.value.length - 1, 1) // Ensure at least 1 to avoid division by zero |
| 104 | +// ); |
| 105 | +// |
| 106 | +// const currentNodeIndex = computed(() => { |
| 107 | +// const activeNode = steps.value[currentStep.value]; |
| 108 | +// if (!activeNode) return 0; |
| 109 | +// |
| 110 | +// // Find the current node's position among relevant nodes |
| 111 | +// const index = relevantNodes.value.findIndex(node => node.nodeId === activeNode.nodeId); |
| 112 | +// return Math.max(index, 0); // Ensure it starts at 0 |
| 113 | +// }); |
| 114 | +// |
| 115 | +// const progress = computed(() => { |
| 116 | +// if (totalNodes.value === 1) return 100; // If there's only one relevant node, treat it as complete |
| 117 | +// return (currentNodeIndex.value / totalNodes.value) * 100; |
| 118 | +// }); |
| 119 | +</script> |
| 120 | + |
| 121 | +<template> |
| 122 | + <!-- TODO --> |
| 123 | + <!-- <ProgressBar :value="progress"></ProgressBar> --> |
| 124 | + |
| 125 | + <div class="stepper-container"> |
| 126 | + <div v-for="(step, index) in steps" :key="step.nodeId"> |
| 127 | + <template v-if="index === currentStep"> |
| 128 | + <!-- Render group nodes --> |
| 129 | + <FormGroup v-if="isGroupNode(step)" :node="step" /> |
| 130 | + |
| 131 | + <!-- Render repeat nodes --> |
| 132 | + <RepeatRange v-else-if="isRepeatRangeNode(step)" :node="step" /> |
| 133 | + |
| 134 | + <!-- Render individual questions --> |
| 135 | + <FormQuestion v-else-if="isControlNode(step)" :question="step" /> |
| 136 | + |
| 137 | + <ExpectModelNode v-else :node="step" /> |
| 138 | + </template> |
| 139 | + </div> |
| 140 | + |
| 141 | + <div class="navigation-buttons"> |
| 142 | + <Button label="Previous" @click="prevStep" :disabled="currentStep === 0" /> |
| 143 | + <Button label="Next" @click="nextStep" :disabled="isCurrentStepValidated && currentStep === steps.length - 1" /> |
| 144 | + </div> |
| 145 | + </div> |
| 146 | +</template> |
| 147 | + |
| 148 | +<style scoped lang="scss"> |
| 149 | +.navigation-buttons { |
| 150 | + display: flex; |
| 151 | + justify-content: space-between; |
| 152 | + width: 100%; |
| 153 | + margin-top: 1rem; |
| 154 | +} |
| 155 | +</style> |
0 commit comments