174 lines
5.2 KiB
JavaScript
174 lines
5.2 KiB
JavaScript
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<Object>} 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)
|
|
}
|
|
|
|
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
|
|
debouncedSync()
|
|
}
|
|
}
|
|
|
|
const handleBlur = () => {
|
|
// When window loses focus, sync immediately
|
|
debouncedSync()
|
|
}
|
|
|
|
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
|
|
}
|
|
} |