opnform-host-nginx/client/components/open/forms/OpenCompleteForm.vue

426 lines
13 KiB
Vue

<template>
<div
v-if="form"
class="open-complete-form"
:dir="form?.layout_rtl ? 'rtl' : 'ltr'"
:style="{ '--font-family': form.font_family, 'direction': form?.layout_rtl ? 'rtl' : 'ltr', '--form-color': form.color }"
>
<ClientOnly>
<Teleport to="head">
<link
v-if="showFontLink && form.font_family"
:key="form.font_family"
:href="getFontUrl"
rel="stylesheet"
crossorigin="anonymous"
referrerpolicy="no-referrer"
>
</Teleport>
</ClientOnly>
<v-transition name="fade" mode="out-in">
<div v-if="isAutoSubmit" key="auto-submit" class="text-center p-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<div v-else key="form-content">
<div v-if="isPublicFormPage && form.is_password_protected">
<p class="form-description text-gray-700 dark:text-gray-300 px-2">
{{ t('forms.password_protected') }}
</p>
<div class="form-group flex flex-wrap w-full">
<div class="relative w-full px-2">
<text-input
:theme="theme"
:form="passwordForm"
name="password"
native-type="password"
label="Password"
/>
</div>
</div>
<div class="flex flex-wrap justify-center w-full text-center">
<open-form-button
:theme="theme"
:color="form.color"
class="my-4"
@click="passwordEntered"
>
{{ t('forms.submit') }}
</open-form-button>
</div>
</div>
<div v-if="!form.is_password_protected && form.password && !hidePasswordDisabledMsg"
class="m-2 my-4">
<UAlert
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'gray', variant: 'link', padded: false }"
color="amber"
variant="subtle"
icon="i-material-symbols-info-outline"
@close="hidePasswordDisabledMsg = true"
title="Password protection has been disabled since you are the owner of this form."
/>
</div>
<UAlert
v-if="isPublicFormPage && (form.is_closed || form.visibility=='closed')"
icon="i-heroicons-lock-closed-20-solid"
color="amber"
variant="subtle"
class="m-2 my-4"
>
<template #description>
<div
class="py-2"
v-html="form.closed_text"
/>
</template>
</UAlert>
<UAlert
v-else-if="isPublicFormPage && form.max_number_of_submissions_reached"
icon="i-heroicons-lock-closed-20-solid"
color="amber"
variant="subtle"
class="m-2 my-4"
>
<template #description>
<div
class="py-2"
v-html="form.max_submissions_reached_text"
/>
</template>
</UAlert>
<form-cleanings
v-if="showFormCleanings"
:hideable="true"
class="mb-4 mx-2"
:form="form"
:specify-form-owner="true"
/>
<v-transition name="fade" v-if="form && !form.is_password_protected">
<div
v-if="!isFormSubmitted"
key="form"
>
<open-form
v-if="formManager && form && shouldDisplayForm"
:form-manager="formManager"
:theme="theme"
@submit="triggerSubmit"
>
<template #submit-btn="{loading}">
<open-form-button
:loading="loading || isProcessing"
:theme="theme"
:color="form.color"
class="mt-2 px-8 mx-1"
:class="submitButtonClass"
@click.prevent="triggerSubmit"
>
{{ form.submit_button_text }}
</open-form-button>
</template>
</open-form>
<p
v-if="!form.no_branding"
class="text-center w-full mt-2"
>
<a
href="https://opnform.com?utm_source=form&utm_content=powered_by"
class="text-gray-400 hover:text-gray-500 dark:text-gray-600 dark:hover:text-gray-500 cursor-pointer hover:underline text-xs"
target="_blank"
>
{{ t('forms.powered_by') }} <span class="font-semibold">{{ t('app.name') }}</span>
</a>
</p>
</div>
<div
v-else
key="submitted"
class="px-2"
>
<TextBlock
v-if="form.submitted_text"
class="form-description text-gray-700 dark:text-gray-300 whitespace-pre-wrap"
:content="form.submitted_text"
:mentions-allowed="true"
:form="form"
:form-data="submittedData"
/>
<open-form-button
v-if="form.re_fillable"
:theme="theme"
:color="form.color"
class="my-4"
@click="restart"
>
{{ form.re_fill_button_text }}
</open-form-button>
<p
v-if="form.editable_submissions && submissionId"
class="mt-5"
>
<a
target="_parent"
:href="form.share_url+'?submission_id='+submissionId"
class="text-nt-blue hover:underline"
>
{{ form.editable_submissions_button_text }}
</a>
</p>
<p
v-if="!form.no_branding"
class="mt-5"
>
<a
target="_parent"
href="https://opnform.com/?utm_source=form&utm_content=create_form_free"
class="text-nt-blue hover:underline"
>
{{ t('forms.create_form_free') }}
</a>
</p>
</div>
</v-transition>
<FirstSubmissionModal
:show="showFirstSubmissionModal"
:form="form"
@close="showFirstSubmissionModal=false"
/>
</div>
</v-transition>
</div>
</template>
<script setup>
import { useFormManager } from '~/lib/forms/composables/useFormManager'
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
import OpenForm from './OpenForm.vue'
import OpenFormButton from './OpenFormButton.vue'
import FormCleanings from '../../pages/forms/show/FormCleanings.vue'
import VTransition from '~/components/global/transitions/VTransition.vue'
import FirstSubmissionModal from '~/components/open/forms/components/FirstSubmissionModal.vue'
import TextBlock from '~/components/forms/TextBlock.vue'
import { useForm } from '~/composables/useForm'
import { useAlert } from '~/composables/useAlert'
import { useI18n } from 'vue-i18n'
import { useIsIframe } from '~/composables/useIsIframe'
import Loader from '~/components/global/Loader.vue'
const props = defineProps({
form: { type: Object, required: true },
mode: {
type: String,
default: FormMode.LIVE,
validator: (value) => Object.values(FormMode).includes(value)
},
submitButtonClass: { type: String, default: '' },
darkMode: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['submitted', 'password-entered', 'restarted'])
const { t, setLocale } = useI18n()
const route = useRoute()
const authStore = useAuthStore()
const alert = useAlert()
const workingFormStore = useWorkingFormStore()
const passwordForm = useForm({ password: null })
const hidePasswordDisabledMsg = ref(false)
const submissionId = ref(route.query.submission_id || null)
const submittedData = ref(null)
const showFirstSubmissionModal = ref(false)
// Check for auto_submit parameter during setup
const isAutoSubmit = ref(import.meta.client && window.location.href.includes('auto_submit=true'))
// Create a reactive reference directly from the prop
const darkModeRef = toRef(props, 'darkMode')
// Add back the local theme computation
const theme = computed(() => {
return new ThemeBuilder(props.form.theme, {
size: props.form.size,
borderRadius: props.form.border_radius
}).getAllComponents()
})
let formManager = null
if (props.form) {
formManager = useFormManager(props.form, props.mode, {
darkMode: darkModeRef
})
formManager.initialize({
submissionId: submissionId.value,
urlParams: import.meta.client ? new URLSearchParams(window.location.search) : null,
})
}
// Share the structure service with the working form store only when in admin edit context
watch(() => formManager?.strategy?.value?.admin?.showAdminControls, (showAdminControls) => {
if (workingFormStore && formManager?.structure && showAdminControls) {
workingFormStore.setStructureService(formManager.structure)
}
}, { immediate: true })
// Add a watcher to update formManager's darkMode whenever darkModeRef changes
watch(darkModeRef, (newDarkMode) => {
if (formManager) {
formManager.setDarkMode(newDarkMode)
}
})
// If auto_submit is true, trigger submit after component is mounted
onMounted(() => {
if (isAutoSubmit.value && formManager) {
// Using nextTick to ensure form is fully rendered and initialized
nextTick(() => {
triggerSubmit()
})
}
})
const isPublicFormPage = computed(() => {
return route.name === 'forms-slug'
})
const getFontUrl = computed(() => {
if(!props.form?.font_family) return null
const family = props.form.font_family.replace(/ /g, '+')
return `https://fonts.googleapis.com/css?family=${family}:wght@400,500,700,800,900&display=swap`
})
const isFormOwner = computed(() => {
return authStore.check && props.form && props.form.creator_id === authStore.user.id
})
const isFormSubmitted = computed(() => formManager?.state.isSubmitted ?? false)
const isProcessing = computed(() => formManager?.state.isProcessing ?? false)
const showFormCleanings = computed(() => formManager?.strategy.value.display.showFormCleanings ?? false)
const showFontLink = computed(() => formManager?.strategy.value.display.showFontLink ?? false)
const shouldDisplayForm = computed(() => {
return (!props.form.is_closed && !props.form.max_number_of_submissions_reached) || formManager?.strategy?.value.admin?.showAdminControls
})
watch(() => props.form.language, (newLanguage) => {
if (newLanguage && typeof newLanguage === 'string') {
setLocale(newLanguage)
} else {
setLocale('en')
}
}, { immediate: true })
onBeforeUnmount(() => {
setLocale('en')
})
const handleScrollToError = () => {
if (import.meta.server) return
nextTick(() => {
const firstErrorElement = document.querySelector('.form-group [error], .form-group .has-error')
if (firstErrorElement) {
const headerOffset = 60 // Offset for fixed headers, adjust as needed
const elementPosition = firstErrorElement.getBoundingClientRect().top
const offsetPosition = elementPosition + window.scrollY - headerOffset
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
})
}
})
}
const triggerSubmit = async () => {
if (!formManager || isProcessing.value) return
formManager.submit({
submissionId: submissionId.value
}).then(result => {
if (result) {
submittedData.value = formManager.form.data()
if (result?.submission_id) {
submissionId.value = result.submission_id
}
if (isFormOwner.value && !useIsIframe() && result?.is_first_submission) {
showFirstSubmissionModal.value = true
}
emit('submitted', true)
} else {
console.warn('Form submission failed via composable, but no error thrown?')
alert.error(t('forms.submission_error'))
}
})
.catch(error => {
if (error.response && error.response.status === 422 && error.data) {
useAlert().formValidationError(error.data)
} else if (error.message) {
useAlert().error(error.message)
}
handleScrollToError()
}).finally(() => {
isAutoSubmit.value = false
})
}
const restart = async () => {
if (!formManager) return
await formManager.restart()
submittedData.value = null
submissionId.value = null
emit('restarted', true)
}
const passwordEntered = () => {
if (passwordForm.password) {
emit('password-entered', passwordForm.password)
} else {
addPasswordError(t('forms.password_required'))
}
}
const addPasswordError = (msg) => {
passwordForm.errors.set('password', msg)
}
defineExpose({
addPasswordError,
restart,
formManager
})
</script>
<style lang="scss">
.open-complete-form {
* {
font-family: var(--font-family) !important;
}
.form-description, .nf-text {
ol {
@apply list-decimal list-inside;
margin-left: 10px;
}
ul {
@apply list-disc list-inside;
margin-left: 10px;
}
}
}
</style>