opnform-host-nginx/resources/js/components/open/forms/OpenForm.vue

506 lines
17 KiB
Vue
Raw Normal View History

2022-09-20 21:59:52 +02:00
<template>
<div v-if="isAutoSubmit">
<p class="text-center p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</p>
</div>
<form v-else-if="dataForm" @submit.prevent="">
2022-09-20 21:59:52 +02:00
<transition name="fade" mode="out-in" appear>
<template v-for="group, groupIndex in fieldGroups">
<div v-if="currentFieldGroupIndex===groupIndex" :key="groupIndex" class="form-group flex flex-wrap w-full">
<template v-for="field in group">
<component :is="getFieldComponents(field)" v-if="getFieldComponents(field)"
:key="field.id + formVersionId" :class="getFieldClasses(field)"
v-bind="inputProperties(field)" :required="isFieldRequired[field.id]"
/>
<template v-else>
<div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id"
class="nf-text w-full px-2 mb-3"
2022-09-20 21:59:52 +02:00
v-html="field.content"
/>
<div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id"
class="nf-code w-full px-2 mb-3"
v-html="field.content"
/>
<div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id" class="border-b my-4 w-full mx-2"/>
<div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id"
:key="field.id" class="my-4 w-full px-2">
2022-09-20 21:59:52 +02:00
<div v-if="!field.image_block" class="p-4 border border-dashed">
Open <b>{{ field.name }}'s</b> block settings to upload image.
</div>
<img v-else :alt="field.name" :src="field.image_block" class="max-w-full">
</div>
</template>
</template>
</div>
</template>
</transition>
<!-- Captcha -->
<template v-if="form.use_captcha && isLastPage">
<div class="mb-3 px-2 mt-2 mx-auto w-max">
<vue-hcaptcha ref="hcaptcha" :sitekey="hCaptchaSiteKey" :theme="darkModeEnabled?'dark':'light'"/>
<has-error :form="dataForm" field="h-captcha-response"/>
2022-09-20 21:59:52 +02:00
</div>
</template>
<!-- Submit, Next and previous buttons -->
<div class="flex flex-wrap justify-center w-full">
<open-form-button v-if="currentFieldGroupIndex>0 && previousFieldsPageBreak && !loading" native-type="button"
:color="form.color" :theme="theme" class="mt-2 px-8 mx-1" @click="previousPage"
2022-09-20 21:59:52 +02:00
>
{{ previousFieldsPageBreak.previous_btn_text }}
</open-form-button>
<slot v-if="isLastPage" name="submit-btn" :submitForm="submitForm"/>
2022-09-20 21:59:52 +02:00
<open-form-button v-else native-type="button" :color="form.color" :theme="theme" class="mt-2 px-8 mx-1"
@click="nextPage"
2022-09-20 21:59:52 +02:00
>
{{ currentFieldsPageBreak.next_btn_text }}
</open-form-button>
<div v-if="!currentFieldsPageBreak && !isLastPage">
Something is wrong with this form structure. If you're the form owner please contact us.
</div>
</div>
</form>
</template>
<script>
import axios from 'axios'
2022-09-20 21:59:52 +02:00
import Form from 'vform'
import OpenFormButton from './OpenFormButton.vue'
2022-09-20 21:59:52 +02:00
import clonedeep from 'clone-deep'
import FormLogicPropertyResolver from '../../../forms/FormLogicPropertyResolver.js'
2022-09-20 21:59:52 +02:00
const VueHcaptcha = () => import('@hcaptcha/vue-hcaptcha')
import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js'
2022-09-20 21:59:52 +02:00
export default {
name: 'OpenForm',
components: {OpenFormButton, VueHcaptcha},
mixins: [FormPendingSubmissionKey],
2022-09-20 21:59:52 +02:00
props: {
form: {
type: Object,
required: true
},
theme: {
type: Object,
required: true
},
loading: {
type: Boolean,
required: true
},
showHidden: {
type: Boolean,
default: false
},
fields: {
type: Array,
required: true
}
},
data() {
2022-09-20 21:59:52 +02:00
return {
dataForm: null,
currentFieldGroupIndex: 0,
/**
* Used to force refresh components by changing their keys
*/
formVersionId: 1,
darkModeEnabled: document.body.classList.contains('dark'),
isAutoSubmit: false
2022-09-20 21:59:52 +02:00
}
},
computed: {
hCaptchaSiteKey: () => window.config.hCaptchaSiteKey,
actualFields() {
2022-09-20 21:59:52 +02:00
return this.fields.filter((field) => {
return this.showHidden || !this.isFieldHidden[field.id]
})
},
/**
* Create field groups (or Page) using page breaks if any
*/
fieldGroups() {
2022-09-20 21:59:52 +02:00
if (!this.actualFields) return []
const groups = []
let currentGroup = []
this.actualFields.forEach((field) => {
currentGroup.push(field)
if (field.type === 'nf-page-break') {
groups.push(currentGroup)
currentGroup = []
}
})
groups.push(currentGroup)
return groups
},
currentFields() {
2022-09-20 21:59:52 +02:00
return this.fieldGroups[this.currentFieldGroupIndex]
},
/**
* Returns the page break block for the current group of fields
*/
currentFieldsPageBreak() {
2022-09-20 21:59:52 +02:00
const block = this.currentFields[this.currentFields.length - 1]
if (block && block.type === 'nf-page-break') return block
return null
},
previousFieldsPageBreak() {
2022-09-20 21:59:52 +02:00
if (this.currentFieldGroupIndex === 0) return null
const previousFields = this.fieldGroups[this.currentFieldGroupIndex - 1]
const block = previousFields[previousFields.length - 1]
if (block && block.type === 'nf-page-break') return block
return null
},
/**
* Returns true if we're on the last page
* @returns {boolean}
*/
isLastPage() {
2022-09-20 21:59:52 +02:00
return this.currentFieldGroupIndex === (this.fieldGroups.length - 1)
},
fieldComponents() {
2022-09-20 21:59:52 +02:00
return {
text: 'TextInput',
number: 'TextInput',
select: 'SelectInput',
multi_select: 'SelectInput',
date: 'DateInput',
files: 'FileInput',
checkbox: 'CheckboxInput',
url: 'TextInput',
email: 'TextInput',
phone_number: 'TextInput',
}
},
isPublicFormPage() {
2022-09-20 21:59:52 +02:00
return this.$route.name === 'forms.show_public'
},
dataFormValue() {
2022-09-20 21:59:52 +02:00
// For get values instead of Id for select/multi select options
const data = this.dataForm.data()
const selectionFields = this.fields.filter((field) => {
return ['select', 'multi_select'].includes(field.type)
})
selectionFields.forEach((field) => {
if (data[field.id] !== undefined && data[field.id] !== null && Array.isArray(data[field.id])) {
data[field.id] = data[field.id].map(option_nfid => {
const tmpop = field[field.type].options.find((op) => {
return (op.id === option_nfid)
})
2022-09-20 21:59:52 +02:00
return (tmpop) ? tmpop.name : option_nfid
})
}
})
return data
},
isFieldHidden() {
2022-09-20 21:59:52 +02:00
const fieldsHidden = {}
this.fields.forEach((field) => {
fieldsHidden[field.id] = (new FormLogicPropertyResolver(field, this.dataFormValue)).isHidden()
})
return fieldsHidden
},
isFieldRequired() {
2022-09-20 21:59:52 +02:00
const fieldsRequired = {}
this.fields.forEach((field) => {
fieldsRequired[field.id] = (new FormLogicPropertyResolver(field, this.dataFormValue)).isRequired()
})
return fieldsRequired
}
},
watch: {
form: {
deep: true,
handler() {
2022-09-20 21:59:52 +02:00
this.initForm()
}
},
fields: {
deep: true,
handler() {
2022-09-20 21:59:52 +02:00
this.initForm()
}
},
theme: {
handler() {
2022-09-20 21:59:52 +02:00
this.formVersionId++
}
},
dataForm: {
deep: true,
handler() {
if (this.isPublicFormPage && this.form && this.dataFormValue) {
try {
window.localStorage.setItem(this.formPendingSubmissionKey, JSON.stringify(this.dataFormValue))
} catch (e) {
}
}
}
},
2022-09-20 21:59:52 +02:00
},
mounted() {
2022-09-20 21:59:52 +02:00
this.initForm()
if(window.location.href.includes('auto_submit=true')){
this.isAutoSubmit = true
this.submitForm()
}
2022-09-20 21:59:52 +02:00
},
methods: {
submitForm() {
2022-09-20 21:59:52 +02:00
if (this.currentFieldGroupIndex !== this.fieldGroups.length - 1) {
return
}
if (this.form.use_captcha) {
this.dataForm['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value
this.$refs.hcaptcha.reset()
}
if (this.form.editable_submissions && this.form.submission_id) {
this.dataForm.submission_id = this.form.submission_id
}
2022-09-20 21:59:52 +02:00
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
},
/**
* If more than one page, show first page with error
*/
onSubmissionFailure() {
this.isAutoSubmit = false
2022-09-20 21:59:52 +02:00
if (this.fieldGroups.length > 1) {
// Find first mistake and show page
let pageChanged = false
this.fieldGroups.forEach((group, groupIndex) => {
group.forEach((field) => {
if (pageChanged) return
if (!pageChanged && this.dataForm.errors.has(field.id)) {
this.currentFieldGroupIndex = groupIndex
pageChanged = true
}
})
})
}
// Scroll to error
const elements = document.getElementsByClassName('has-error')
if (elements.length > 0) {
window.scroll({
top: window.scrollY + elements[0].getBoundingClientRect().top - 60,
behavior: 'smooth'
})
}
},
async getSubmissionData() {
if (!this.form || !this.form.editable_submissions || !this.form.submission_id) { return null }
await this.$store.dispatch('open/records/loadRecord',
axios.get('/api/forms/' + this.form.slug + '/submissions/' + this.form.submission_id).then((response) => {
return { submission_id: this.form.submission_id, ...response.data.data }
})
)
return this.$store.getters['open/records/getById'](this.form.submission_id)
},
async initForm() {
if (this.isPublicFormPage && this.form.editable_submissions) {
const urlParam = new URLSearchParams(window.location.search)
if (urlParam && urlParam.get('submission_id')) {
this.form.submission_id = urlParam.get('submission_id')
const data = await this.getSubmissionData()
if (data !== null && data) {
this.dataForm = new Form(data)
return
}
}
}
if (this.isPublicFormPage) {
let pendingData
try {
pendingData = window.localStorage.getItem(this.formPendingSubmissionKey)
} catch (e) {
pendingData = null
}
if (pendingData !== null && pendingData) {
pendingData = JSON.parse(pendingData)
this.fields.forEach((field) => {
if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
const dateObj = new Date()
let currentDate = dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0')
if(field.with_time === true){
currentDate += 'T' + String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0');
}
pendingData[field.id] = currentDate
}
})
this.dataForm = new Form(pendingData)
return
}
}
2022-09-20 21:59:52 +02:00
const formData = clonedeep(this.dataForm ? this.dataForm.data() : {})
let urlPrefill = null
if (this.isPublicFormPage && this.form.is_pro) {
urlPrefill = new URLSearchParams(window.location.search)
}
this.fields.forEach((field) => {
if (field.type.startsWith('nf-')) {
return
}
if (urlPrefill && urlPrefill.has(field.id)) {
// Url prefills
if (field.type === 'checkbox') {
if (urlPrefill.get(field.id) === 'false' || urlPrefill.get(field.id) === '0') {
formData[field.id] = false
} else if (urlPrefill.get(field.id) === 'true' || urlPrefill.get(field.id) === '1') {
formData[field.id] = true
}
} else {
formData[field.id] = urlPrefill.get(field.id)
}
} else if (urlPrefill && urlPrefill.has(field.id + '[]')) {
// Array url prefills
formData[field.id] = urlPrefill.getAll(field.id + '[]')
} else if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
const dateObj = new Date()
2023-02-07 13:40:46 +01:00
let currentDate = dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0')
2023-02-07 13:40:46 +01:00
if(field.with_time === true){
currentDate += 'T' + String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0');
}
formData[field.id] = currentDate
2022-09-20 21:59:52 +02:00
} else { // Default prefill if any
formData[field.id] = field.prefill
}
})
this.dataForm = new Form(formData)
},
/**
* Get the right input component for the field/options combination
*/
getFieldComponents(field) {
2022-09-20 21:59:52 +02:00
if (field.type === 'text' && field.multi_lines) {
return 'TextAreaInput'
}
if (field.type === 'url' && field.file_upload) {
return 'FileInput'
}
if (field.type === 'number' && field.is_rating && field.rating_max_value) {
return 'RatingInput'
}
if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) {
return 'FlatSelectInput'
}
if (field.type === 'checkbox' && field.use_toggle_switch) {
return 'ToggleSwitchInput'
}
if (field.type === 'signature') {
return 'SignatureInput'
}
2022-09-20 21:59:52 +02:00
return this.fieldComponents[field.type]
},
getFieldClasses(field) {
2022-09-20 21:59:52 +02:00
if (!field.width || field.width === 'full') return 'w-full px-2'
else if (field.width === '1/2') {
return 'w-full sm:w-1/2 px-2'
} else if (field.width === '1/3') {
return 'w-full sm:w-1/3 px-2'
} else if (field.width === '2/3') {
return 'w-full sm:w-2/3 px-2'
} else if (field.width === '1/4') {
return 'w-full sm:w-1/4 px-2'
} else if (field.width === '3/4') {
return 'w-full sm:w-3/4 px-2'
}
},
/**
* Get the right input component options for the field/options
*/
inputProperties(field) {
2022-09-20 21:59:52 +02:00
const inputProperties = {
key: field.id,
name: field.id,
form: this.dataForm,
label: (field.hide_field_name) ? null : field.name + (this.isFieldHidden[field.id] ? ' (Hidden Field)' : ''),
color: this.form.color,
placeholder: field.placeholder,
help: field.help,
uppercaseLabels: this.form.uppercase_labels,
theme: this.theme,
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000,
showCharLimit: field.show_char_limit || false
}
if (['select', 'multi_select'].includes(field.type)) {
inputProperties.options = (field.hasOwnProperty(field.type))
? field[field.type].options.map(option => {
return {
name: option.name,
value: option.name
}
})
: []
inputProperties.multiple = (field.type === 'multi_select')
inputProperties.allowCreation = (field.allow_creation === true)
inputProperties.searchable = (inputProperties.options.length > 4)
} else if (field.type === 'date') {
if (field.with_time) {
inputProperties.withTime = true
} else if (field.date_range) {
inputProperties.dateRange = true
}
if (field.disable_past_dates) {
inputProperties.disablePastDates = true
} else if (field.disable_future_dates) {
inputProperties.disableFutureDates = true
}
2022-09-20 21:59:52 +02:00
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
inputProperties.mbLimit = 5
inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : ""
} else if (field.type === 'number' && field.is_rating) {
inputProperties.numberOfStars = parseInt(field.rating_max_value)
}
return inputProperties
},
previousPage() {
2022-09-20 21:59:52 +02:00
this.currentFieldGroupIndex -= 1
return false
},
nextPage() {
2022-09-20 21:59:52 +02:00
this.currentFieldGroupIndex += 1
return false
}
}
}
</script>
<style lang="scss">
.nf-text {
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
</style>