opnform-host-nginx/client/lib/forms/composables/useFormStructure.js

366 lines
13 KiB
JavaScript

import { computed, toValue } from 'vue'
import FormLogicPropertyResolver from '~/lib/forms/FormLogicPropertyResolver.js'
/**
* @fileoverview Composable responsible for analyzing and managing the structural aspects of a form,
* including page breaks, field grouping, page boundaries, and determining field locations.
*/
export function useFormStructure(formConfig, managerState, formData) {
const form = computed(() => toValue(formConfig) || { properties: [] })
/**
* Checks if a field is hidden based on form logic.
* Uses FormLogicPropertyResolver.
* @param {Object} field - The field configuration object.
* @returns {Boolean} True if the field is hidden, false otherwise.
*/
const isFieldHidden = (field) => {
try {
// Use the formData ref passed into the composable
const currentFormData = toValue(formData) || {}
return new FormLogicPropertyResolver(field, currentFormData).isHidden()
} catch (e) {
console.error("Error checking if field is hidden:", field?.id, e)
return field?.hidden || false // Fallback
}
}
/**
* Calculates the groups of fields based on non-hidden page breaks.
* @returns {Array<Array<Object>>} Nested array where each inner array represents a page.
*/
const calculateFieldGroups = () => {
const properties = form.value.properties || []
if (properties.length === 0) return [[]]
const groups = []
let currentGroup = []
properties.forEach((field, index) => {
currentGroup.push(field)
// Check if the field is a page break AND it's not hidden
if (field.type === 'nf-page-break' && !isFieldHidden(field)) {
groups.push([...currentGroup])
if (index < properties.length - 1) {
currentGroup = []
}
}
})
// Add the last group if it's not empty AND the last field wasn't a non-hidden page break
const lastProperty = properties[properties.length - 1]
if (currentGroup.length > 0 && (!lastProperty || lastProperty.type !== 'nf-page-break' || isFieldHidden(lastProperty))) {
groups.push(currentGroup)
}
// Ensure at least one group exists
if (groups.length === 0) {
groups.push([])
}
return groups
}
/**
* Reactive computed property holding the field groups.
*/
const fieldGroups = computed(calculateFieldGroups)
/**
* Reactive computed property holding the total number of pages.
*/
const pageCount = computed(() => {
const groups = fieldGroups.value
const count = groups ? groups.length : 0
return count
})
/**
* Calculates the start and end indices for each page.
* @returns {Array<Object>} Array of boundary objects { start, end }.
*/
const calculatePageBoundaries = () => {
const properties = form.value.properties || []
if (properties.length === 0) {
return [{ start: 0, end: -1 }] // Empty form
}
const boundaries = []
let startIndex = 0
let visibleBreakFound = false
properties.forEach((field, index) => {
// Check if the field is a page break AND it's not hidden
if (field.type === 'nf-page-break' && !isFieldHidden(field)) {
visibleBreakFound = true
// The page ends *at* the page break field
boundaries.push({ start: startIndex, end: index })
// The next page starts *after* the page break field
startIndex = index + 1
}
})
// If no visible page breaks were found, the entire form is a single page
if (!visibleBreakFound) {
return [{ start: 0, end: properties.length - 1 }]
}
// Add the boundary for the last page if there are fields after the last visible break
if (startIndex < properties.length) {
boundaries.push({ start: startIndex, end: properties.length - 1 })
}
// If the last field was a visible page break, startIndex will equal properties.length,
// and we don't add an extra empty page boundary.
// Safety check: Ensure at least one boundary exists if properties are not empty
if (boundaries.length === 0 && properties.length > 0) {
return [{ start: 0, end: properties.length - 1 }]
}
return boundaries
}
/**
* Reactive computed property holding the page boundaries.
*/
const pageBoundaries = computed(calculatePageBoundaries)
/**
* Reactive computed property for the page break field ending the current page.
*/
const currentPageBreak = computed(() => {
const groups = fieldGroups.value
if (!managerState || !groups) return null
const currentPageIndex = managerState.currentPage
if (currentPageIndex < 0 || currentPageIndex >= groups.length) return null
const currentPageFields = groups[currentPageIndex] || []
if (currentPageFields.length === 0) return null
const lastField = currentPageFields[currentPageFields.length - 1]
// It's the current page break only if it's a page break and *not hidden*
return (lastField && lastField.type === 'nf-page-break' && !isFieldHidden(lastField)) ? lastField : null
})
/**
* Reactive computed property for the page break field ending the previous page.
*/
const previousPageBreak = computed(() => {
const groups = fieldGroups.value
if (!managerState || !groups || managerState.currentPage <= 0) return null
const previousPageIndex = managerState.currentPage - 1
if (previousPageIndex < 0 || previousPageIndex >= groups.length) return null
const previousPageFields = groups[previousPageIndex] || []
if (previousPageFields.length === 0) return null
const lastField = previousPageFields[previousPageFields.length - 1]
// It's the previous page break only if it's a page break and *not hidden*
return (lastField && lastField.type === 'nf-page-break' && !isFieldHidden(lastField)) ? lastField : null
})
/**
* Reactive computed property indicating if the current page is the last one.
*/
const isLastPage = computed(() => {
if (managerState?.currentPage === undefined || managerState?.currentPage === null || pageCount.value === undefined) {
// console.warn('[useFormStructure] isLastPage: Invalid state, returning default true.');
return true // Default true for safety in simple forms
}
const result = managerState.currentPage === pageCount.value - 1
return result
})
/**
* Reactive computed property checking if current page has a payment block
*/
const currentPageHasPaymentBlock = computed(() => {
if (managerState?.currentPage === undefined || managerState?.currentPage === null) return false
return hasPaymentBlock(managerState.currentPage)
})
/**
* Reactive computed property returning the payment block from the current page, if any
*/
const currentPagePaymentBlock = computed(() => {
if (managerState?.currentPage === undefined || managerState?.currentPage === null) return undefined
return getPaymentBlock(managerState.currentPage)
})
/**
* Gets the fields for a specific page index.
* @param {Number} pageIndex - The index of the page.
* @returns {Array<Object>} Array of field objects for the page.
*/
const getPageFields = (pageIndex) => {
const groups = fieldGroups.value
if (!groups) {
console.warn("useFormStructure: getPageFields called but fieldGroups is undefined.")
return []
}
return groups[pageIndex] || []
}
/**
* Determines the page index for a given field index within the form properties array.
* @param {Number} fieldIndex - The index of the field in the form.properties array.
* @returns {Number} The page index (0-based).
*/
const getPageForField = (fieldIndex) => {
// Basic validation for field index
if (fieldIndex === null || fieldIndex === undefined ||
typeof fieldIndex !== 'number' || isNaN(fieldIndex) || fieldIndex < 0) {
console.warn(`Invalid field index passed to getPageForField: ${fieldIndex}`)
return 0 // Default to first page for invalid indexes
}
const properties = form.value.properties || []
if (properties.length === 0 || fieldIndex >= properties.length) {
return 0 // Default to first page
}
const boundaries = pageBoundaries.value
if (!boundaries || boundaries.length === 0) return 0
// If only one boundary (covers all), it's page 0
if (boundaries.length === 1 && boundaries[0].start === 0) {
return 0
}
for (let i = 0; i < boundaries.length; i++) {
const { start, end } = boundaries[i]
if (fieldIndex >= start && fieldIndex <= end) {
return i
}
}
// Fallback: Should technically not be reached with correct boundaries
console.warn(`[useFormStructure] getPageForField: Field index ${fieldIndex} not found within calculated boundaries. Returning last page index.`)
return Math.max(0, boundaries.length - 1)
}
/**
* Checks if a given page contains a payment block.
* @param {Number} pageIndex - The page index.
* @returns {Boolean} True if a payment block exists on the page.
*/
const hasPaymentBlock = (pageIndex) => {
return getPageFields(pageIndex).some(field => field?.type === 'payment')
}
/**
* Gets the payment block field object from a given page.
* @param {Number} pageIndex - The page index.
* @returns {Object|undefined} The payment field object or undefined if not found.
*/
const getPaymentBlock = (pageIndex) => {
return getPageFields(pageIndex).find(field => field?.type === 'payment')
}
/**
* Determines the target index in the flat form.properties array for drag/drop operations.
* @param {number} currentFieldPageIndex - The relative index of the field within the current page's field list.
* @param {number} selectedFieldIndex - The original flat index of the field being dragged (often not needed here).
* @param {number} currentPageIndex - The index of the page where the drop occurs.
* @returns {number} The absolute index in the form.properties array.
*/
const getTargetDropIndex = (relativeDropIndex, targetPageIndex) => {
const groups = fieldGroups.value
if (!groups) return relativeDropIndex // Fallback
let precedingFields = 0
for(let i = 0; i < targetPageIndex; i++) {
precedingFields += groups[i]?.length || 0
}
return precedingFields + relativeDropIndex
}
/**
* Sets the current page to the page containing the specified field.
* @param {Number} fieldIndex - The index of the field in the form.properties array.
* @returns {Number} The page index that was set.
*/
const setPageForField = (fieldIndex) => {
// Get the page index, with additional validation
const pageIndex = getPageForField(fieldIndex)
// Ensure we have a valid numeric page index
if (typeof pageIndex !== 'number' || isNaN(pageIndex) || pageIndex < 0) {
console.warn('[useFormStructure] setPageForField: Invalid page index', pageIndex)
return
}
// Update the manager state with validated page index - DON'T USE toValue HERE
if (managerState && managerState.currentPage !== undefined) {
managerState.currentPage = pageIndex
}
return pageIndex
}
/**
* Determines the correct index to insert a new field.
* Considers selected field index and current page boundaries.
* @param {Number|null} selectedFieldIndex - The index of the currently selected field in form.properties.
* @param {Number} currentPageIndex - The current page index.
* @param {Number|null} [explicitIndex=null] - An explicitly provided insert index.
* @returns {Number} The calculated index for insertion.
*/
const determineInsertIndex = (selectedFieldIndex, currentPageIndex, explicitIndex = null) => {
if (explicitIndex !== null && typeof explicitIndex === 'number') {
return explicitIndex
}
if (selectedFieldIndex !== null && selectedFieldIndex !== undefined && selectedFieldIndex >= 0) {
return selectedFieldIndex + 1
}
const properties = form.value.properties || []
if (properties.length === 0) {
return 0
}
const boundaries = pageBoundaries.value
// Use managerState directly without toValue
const pageIdx = currentPageIndex ?? managerState?.currentPage ?? 0 // Use provided or state page index
if (!boundaries || boundaries.length === 0 || pageIdx >= boundaries.length || pageIdx < 0) {
// Fallback to end of the form if boundaries/page index is invalid
return properties.length
}
const currentBoundary = boundaries[pageIdx]
// Insert at the end of the current page (index after the last field of that page)
return currentBoundary.end + 1
}
// --- Exposed API ---
return {
// Reactive Computed Properties
fieldGroups,
pageCount,
pageBoundaries,
currentPageBreak,
previousPageBreak,
isLastPage,
currentPage: computed(() => managerState?.currentPage ?? 0),
currentPageHasPaymentBlock,
currentPagePaymentBlock,
// Methods
getPageFields, // Get fields for a specific page
getPageForField, // Find which page a field index belongs to
hasPaymentBlock, // Check if a page has a payment block
getPaymentBlock, // Get the payment block from a page
isFieldHidden, // Check if a specific field is hidden by logic
getTargetDropIndex, // Calculate absolute index for drag/drop
determineInsertIndex, // Calculate where to insert a new field
setPageForField // Set the current page to the page containing the specified field
}
}