Auto-resize iframes, fix custom code, fix create form initial properties

This commit is contained in:
Julien Nahum 2024-02-01 18:21:30 +01:00
parent de3e2d69c0
commit a650228a67
10 changed files with 132 additions and 112 deletions

View File

@ -98,7 +98,7 @@
</g> </g>
<defs> <defs>
<clipPath id="clip0_1027_7292"> <clipPath id="clip0_1027_7292">
<rect width="24" height="24" fill="white" /> <rect width="24" height="24" fill="white"/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
@ -138,7 +138,7 @@
</g> </g>
<defs> <defs>
<clipPath id="clip0_1027_7210"> <clipPath id="clip0_1027_7210">
<rect width="24" height="24" fill="white" /> <rect width="24" height="24" fill="white"/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
@ -172,7 +172,7 @@ import clonedeep from 'clone-deep'
import EditableDiv from '~/components/global/EditableDiv.vue' import EditableDiv from '~/components/global/EditableDiv.vue'
import VButton from '~/components/global/VButton.vue' import VButton from '~/components/global/VButton.vue'
draggable.compatConfig = { MODE: 3 } draggable.compatConfig = {MODE: 3}
export default { export default {
name: 'FormFieldsEditor', name: 'FormFieldsEditor',
components: { components: {
@ -182,7 +182,7 @@ export default {
EditableDiv EditableDiv
}, },
setup () { setup() {
const workingFormStore = useWorkingFormStore() const workingFormStore = useWorkingFormStore()
return { return {
route: useRoute(), route: useRoute(),
@ -191,21 +191,21 @@ export default {
} }
}, },
data () { data() {
return { return {
removing: null removing: null
} }
}, },
mounted () { mounted() {
this.init() this.init()
}, },
methods: { methods: {
onChangeName (field, newName) { onChangeName(field, newName) {
field.name = newName field.name = newName
}, },
toggleHidden (field) { toggleHidden(field) {
field.hidden = !field.hidden field.hidden = !field.hidden
if (field.hidden) { if (field.hidden) {
field.required = false field.required = false
@ -214,69 +214,27 @@ export default {
field.generates_auto_increment_id = false field.generates_auto_increment_id = false
} }
}, },
toggleRequired (field) { toggleRequired(field) {
field.required = !field.required field.required = !field.required
if (field.required) { if (field.required) {
field.hidden = false field.hidden = false
} }
}, },
getDefaultFields () { init() {
return [ if (!this.form.properties) {
{ return
name: 'Name',
type: 'text',
hidden: false,
required: true,
id: this.generateUUID()
},
{
name: 'Email',
type: 'email',
hidden: false,
id: this.generateUUID()
},
{
name: 'Message',
type: 'text',
hidden: false,
multi_lines: true,
id: this.generateUUID()
}
]
},
init () {
if (this.route.name === 'forms-create' || this.route.name === 'forms-create-guest') { // Set Default fields
if (!this.form.properties || this.form.properties.length===0) {
this.form.properties = this.getDefaultFields()
}
} else {
this.form.properties = this.form.properties.map((field) => {
// Add more field properties
field.placeholder = field.placeholder || null
field.prefill = field.prefill || null
field.help = field.help || null
field.help_position = field.help_position || 'below_input'
return field
})
} }
}, this.form.properties = this.form.properties.map((field) => {
generateUUID () { // Add more field properties
let d = new Date().getTime()// Timestamp field.placeholder = field.placeholder || null
let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0// Time in microseconds since page-load or 0 if unsupported field.prefill = field.prefill || null
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { field.help = field.help || null
let r = Math.random() * 16// random number between 0 and 16 field.help_position = field.help_position || 'below_input'
if (d > 0) { // Use timestamp until depleted
r = (d + r) % 16 | 0 return field
d = Math.floor(d / 16)
} else { // Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0
d2 = Math.floor(d2 / 16)
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
}) })
}, },
formatType (field) { formatType(field) {
let type = field.type.replace('_', ' ') let type = field.type.replace('_', ' ')
if (!type.startsWith('nf')) { if (!type.startsWith('nf')) {
type = type + ' Input' type = type + ' Input'
@ -288,17 +246,17 @@ export default {
} }
return type return type
}, },
editOptions (index) { editOptions(index) {
this.workingFormStore.openSettingsForField(index) this.workingFormStore.openSettingsForField(index)
}, },
removeBlock (blockIndex) { removeBlock(blockIndex) {
this.form.properties.splice(blockIndex, 1) this.form.properties.splice(blockIndex, 1)
this.closeSidebar() this.closeSidebar()
}, },
closeSidebar () { closeSidebar() {
this.workingFormStore.closeEditFieldSidebar() this.workingFormStore.closeEditFieldSidebar()
}, },
openAddFieldSidebar () { openAddFieldSidebar() {
this.workingFormStore.openAddFieldSidebar(null) this.workingFormStore.openAddFieldSidebar(null)
} }
} }

View File

@ -27,7 +27,7 @@
</template> </template>
</template> </template>
<div v-if="state=='default'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8"> <div v-if="state=='default'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
<div v-track.select_form_base="{base:'contact-form'}" <div v-track.select_form_base="{base:'contact-form'}" role="button"
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="$emit('close')" class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="$emit('close')"
> >
<div class="p-4"> <div class="p-4">
@ -41,7 +41,7 @@
</p> </p>
</div> </div>
<div v-if="aiFeaturesEnabled" v-track.select_form_base="{base:'ai'}" <div v-if="aiFeaturesEnabled" v-track.select_form_base="{base:'ai'}"
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="state='ai'" class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" role="button" @click="state='ai'"
> >
<div class="p-4 relative"> <div class="p-4 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
@ -71,7 +71,7 @@
</div> </div>
</div> </div>
<div v-else-if="state=='ai'"> <div v-else-if="state=='ai'">
<a class="absolute top-4 left-4" href="#" @click.prevent="state='default'"> <a class="absolute top-4 left-4" href="#" role="button" @click.prevent="state='default'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 inline -mt-1"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 inline -mt-1">
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z" clip-rule="evenodd" />
</svg> </svg>

View File

@ -5,7 +5,7 @@
<copy-content :content="embedCode" buttonText="Copy Code"> <copy-content :content="embedCode" buttonText="Copy Code">
<template #icon> <template #icon>
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 18 18" fill="none" <svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 18 18" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path <path
d="M11.0833 11.5L13.5833 9L11.0833 6.5M6.91667 6.5L4.41667 9L6.91667 11.5M5.5 16.5H12.5C13.9001 16.5 14.6002 16.5 15.135 16.2275C15.6054 15.9878 15.9878 15.6054 16.2275 15.135C16.5 14.6002 16.5 13.9001 16.5 12.5V5.5C16.5 4.09987 16.5 3.3998 16.2275 2.86502C15.9878 2.39462 15.6054 2.01217 15.135 1.77248C14.6002 1.5 13.9001 1.5 12.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V12.5C1.5 13.9001 1.5 14.6002 1.77248 15.135C2.01217 15.6054 2.39462 15.9878 2.86502 16.2275C3.3998 16.5 4.09987 16.5 5.5 16.5Z" d="M11.0833 11.5L13.5833 9L11.0833 6.5M6.91667 6.5L4.41667 9L6.91667 11.5M5.5 16.5H12.5C13.9001 16.5 14.6002 16.5 15.135 16.2275C15.6054 15.9878 15.9878 15.6054 16.2275 15.135C16.5 14.6002 16.5 13.9001 16.5 12.5V5.5C16.5 4.09987 16.5 3.3998 16.2275 2.86502C15.9878 2.39462 15.6054 2.01217 15.135 1.77248C14.6002 1.5 13.9001 1.5 12.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V12.5C1.5 13.9001 1.5 14.6002 1.77248 15.135C2.01217 15.6054 2.39462 15.9878 2.86502 16.2275C3.3998 16.5 4.09987 16.5 5.5 16.5Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/> stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
@ -15,41 +15,40 @@
</copy-content> </copy-content>
</div> </div>
</template> </template>
<script> <script>
import CopyContent from '../../../open/forms/components/CopyContent.vue' import CopyContent from '../../../open/forms/components/CopyContent.vue'
import {appUrl} from "~/lib/utils.js";
export default { export default {
name: 'EmbedCode', name: 'EmbedCode',
components: { CopyContent }, components: {CopyContent},
props: { props: {
form: { type: Object, required: true }, form: {type: Object, required: true},
extraQueryParam: { type: String, default: '' } extraQueryParam: {type: String, default: ''}
},
data: () => ({
autoresizeIframe: false
}),
computed: {
embedCode() {
return `
<script type="text/javascript" src="${appUrl('/widgets/iframeResize.min.js')}"><\/script>
${this.iframeCode}
<script type="text/javascript">iFrameResize({log: false, checkOrigin: false}, "#${this.iframeId}");<\/script>
`
}, },
iframeCode() {
data: () => ({ const share_url = (this.extraQueryParam) ? this.form.share_url + "?" + this.extraQueryParam : this.form.share_url + this.extraQueryParam
return '<iframe style="border:none;width:100%;" frameborder="0" width="100%" frameborder="0" id="' + this.iframeId + '" src="' + share_url + '"></iframe>'
}),
computed: {
embedCode() {
const share_url = (this.extraQueryParam) ? this.form.share_url + "?" + this.extraQueryParam : this.form.share_url + this.extraQueryParam
return '<iframe style="border:none;width:100%;" height="' + this.formHeight + 'px" src="' + share_url + '"></iframe>'
},
formHeight() {
let height = 200
if (!this.form.hide_title && !this.extraQueryParam) {
height += 60
}
height += this.form.properties.filter((property) => {
return !property.hidden
}).length * 70
return height
}
}, },
iframeId() {
return 'form-' + this.form.slug
}
},
methods: {} methods: {}
} }
</script> </script>

View File

@ -1,11 +1,12 @@
import {generateUUID} from "~/lib/utils.js";
export const initForm = (defaultValue = {}) => { export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
return useForm({ return useForm({
title: 'My Form', title: 'My Form',
description: null, description: null,
visibility: 'public', visibility: 'public',
workspace_id: null, workspace_id: null,
properties: [], properties: withDefaultProperties ? getDefaultProperties() :[],
notifies: false, notifies: false,
slack_notifies: false, slack_notifies: false,
@ -52,3 +53,28 @@ export const initForm = (defaultValue = {}) => {
...defaultValue ...defaultValue
}) })
} }
function getDefaultProperties () {
return [
{
name: 'Name',
type: 'text',
hidden: false,
required: true,
id: generateUUID()
},
{
name: 'Email',
type: 'email',
hidden: false,
id: generateUUID()
},
{
name: 'Message',
type: 'text',
hidden: false,
multi_lines: true,
id: generateUUID()
}
]
}

16
client/lib/utils.js vendored
View File

@ -13,6 +13,22 @@ export const hash = (str, seed = 0) => {
return 4294967296 * (2097151 & h2) + (h1 >>> 0); return 4294967296 * (2097151 & h2) + (h1 >>> 0);
} }
export const generateUUID = () => {
let d = new Date().getTime()// Timestamp
let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0// Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16// random number between 0 and 16
if (d > 0) { // Use timestamp until depleted
r = (d + r) % 16 | 0
d = Math.floor(d / 16)
} else { // Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0
d2 = Math.floor(d2 / 16)
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
}
/* /*
* Url and domain related utils * Url and domain related utils
*/ */

View File

@ -111,13 +111,6 @@ const loadForm = async (setup=false) => {
// Adapt page to form: colors, custom code etc // Adapt page to form: colors, custom code etc
handleDarkMode(form.value.dark_mode) handleDarkMode(form.value.dark_mode)
handleTransparentMode(form.value.transparent_background) handleTransparentMode(form.value.transparent_background)
if (process.server) return
if (form.value.custom_code) {
const scriptEl = document.createRange().createContextualFragment(form.value.custom_code)
document.head.append(scriptEl)
}
if (!isIframe) focusOnFirstFormElement()
} }
await loadForm(true) await loadForm(true)
@ -127,6 +120,14 @@ onMounted(() => {
if (form.value) { if (form.value) {
handleDarkMode(form.value?.dark_mode) handleDarkMode(form.value?.dark_mode)
handleTransparentMode(form.value?.transparent_background) handleTransparentMode(form.value?.transparent_background)
if (process.client) {
if (form.value.custom_code) {
const scriptEl = document.createRange().createContextualFragment(form.value.custom_code)
document.head.append(scriptEl)
}
if (!isIframe) focusOnFirstFormElement()
}
} }
}) })
@ -165,6 +166,9 @@ useHead({
return titleChunk return titleChunk
} }
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm'; return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm';
} },
... form.value.custom_code ? {
script: [ { src: '/widgets/iframeResizer.contentWindow.min.js' } ]
} : {}
}) })
</script> </script>

View File

@ -75,7 +75,7 @@ onMounted(() => {
is_pro: false is_pro: false
}]) }])
form.value = initForm() form.value = initForm({}, true)
if (route.query.template !== undefined && route.query.template) { if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template) const template = templatesStore.getByKey(route.query.template)
if (template && template.structure) { if (template && template.structure) {

View File

@ -92,7 +92,7 @@ onMounted(() => {
formStore.loadAll(workspace.value.id) formStore.loadAll(workspace.value.id)
} }
form.value = initForm({workspace_id: workspace.value?.id}) form.value = initForm({workspace_id: workspace.value?.id}, true)
formInitialHash.value = hash(JSON.stringify(form.value.data())) formInitialHash.value = hash(JSON.stringify(form.value.data()))
if (route.query.template !== undefined && route.query.template) { if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template) const template = templatesStore.getByKey(route.query.template)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long