import { opnFetch } from "./../useOpnApi.js" import { watch, onBeforeUnmount, ref, toValue } from 'vue' // Create a Map to store submission hashes for different forms // This map might be better managed globally or passed if needed across instances const submissionHashes = ref(new Map()) /** * Composable for handling partial form submissions (auto-syncing to server). * * @param {Object} formConfig - The form configuration object (not reactive needed here, just for slug). * @param {import('vue').ComputedRef} formDataRef - Computed reference to the reactive form data. * @param {Object} pendingSubmissionService - The instantiated service from usePendingSubmission. */ export function usePartialSubmission(formConfig, formDataRef, pendingSubmissionService) { let syncTimeout = null let dataWatcher = null const getSubmissionHash = () => { // Prioritize hash from the pendingSubmission service (localStorage) const storedHash = pendingSubmissionService.getSubmissionHash() if (storedHash) return storedHash // Fallback to the in-memory map for this instance if needed (should ideally be synced) const key = pendingSubmissionService.formPendingSubmissionKey?.value // Use optional chaining return key ? submissionHashes.value.get(key) : null } const setSubmissionHash = (hash) => { // Set in both localStorage (via service) and the local map pendingSubmissionService.setSubmissionHash(hash) const key = pendingSubmissionService.formPendingSubmissionKey?.value if (key) { submissionHashes.value.set(key, hash) } } const debouncedSync = () => { // Clear existing timeout to reset the timer if (syncTimeout) { clearTimeout(syncTimeout) } // Set a new timeout - increased to 2 seconds for less frequent syncing syncTimeout = setTimeout(() => { syncToServer() }, 2000) // 2 second debounce } // Add a function to execute sync immediately without debouncing // This is used for critical moments like page unload const syncImmediately = () => { if (syncTimeout) { clearTimeout(syncTimeout) syncTimeout = null } return syncToServer() } const syncToServer = async () => { const config = toValue(formConfig) // Ensure we have the latest config value // Check if partial submissions are enabled and if we have data if (!config?.enable_partial_submissions) { return } // Get current form data from the reactive ref const currentData = formDataRef.value // Directly use .value from computed ref // Skip if no data or empty data if (!currentData || Object.keys(currentData).length === 0) { return } try { const response = await opnFetch(`/forms/${config.slug}/answer`, { method: "POST", body: { ...currentData, 'is_partial': true, 'submission_hash': getSubmissionHash() // Use the updated getter } }) if (response.submission_hash) { setSubmissionHash(response.submission_hash) // Use the updated setter } } catch (error) { console.error(error) } } // Add these handlers as named functions so we can remove them later const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { // When tab becomes hidden, sync immediately syncImmediately() } } const handleBlur = () => { // When window loses focus, sync immediately syncImmediately() } const handleBeforeUnload = () => { // Before page unloads, sync immediately syncImmediately() } const startSync = () => { if (dataWatcher || import.meta.server) { // Prevent starting multiple times or on server return } // Initial sync debouncedSync() // Watch formDataRef using Vue's reactivity dataWatcher = watch( formDataRef, (_newValue) => { debouncedSync() }, { deep: true } ) // Add event listeners for critical moments document.addEventListener('visibilitychange', handleVisibilityChange) window.addEventListener('blur', handleBlur) window.addEventListener('beforeunload', handleBeforeUnload) } const stopSync = () => { if (import.meta.server) return // Final sync before stopping if we have a hash if (getSubmissionHash()) { syncImmediately() } const key = pendingSubmissionService.formPendingSubmissionKey?.value if (key) { submissionHashes.value.delete(key) // Clear from instance map on stop } if (dataWatcher) { dataWatcher() dataWatcher = null } if (syncTimeout) { clearTimeout(syncTimeout) syncTimeout = null } // Remove event listeners document.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('blur', handleBlur) window.removeEventListener('beforeunload', handleBeforeUnload) } // Ensure cleanup when component is unmounted onBeforeUnmount(() => { stopSync() }) return { startSync, stopSync, syncToServer: debouncedSync, // Expose the debounced version externally syncImmediately, // Also expose the immediate sync for critical situations getSubmissionHash, // Use the getter that prioritizes localStorage setSubmissionHash // Use the setter that updates both } }