Add block shortcut (#200)

* Add block shortcut

* Add block shortcut

* Add block modal as sidebar

* add block sidebar UI changes

* Clean spacing add form block sidebar

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
formsdev 2023-09-18 19:03:32 +05:30 committed by GitHub
parent 402e74eaad
commit 52c9f054a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 346 additions and 382 deletions

View File

@ -6,8 +6,18 @@
<div v-if="adminPreview" <div v-if="adminPreview"
class="absolute -translate-x-full top-0 bottom-0 opacity-0 group-hover/nffield:opacity-100 transition-opacity mb-4" class="absolute -translate-x-full top-0 bottom-0 opacity-0 group-hover/nffield:opacity-100 transition-opacity mb-4"
> >
<div class="flex flex-col lg:flex-row bg-white rounded-md"> <div class="flex flex-col bg-white rounded-md" :class="{'lg:flex-row':!fieldSideBarOpened, 'xl:flex-row':fieldSideBarOpened}">
<div class="p-2 lg:pr-1 text-gray-300 hover:text-blue-500 cursor-pointer" role="button" <div class="p-2 -mr-3 -mb-2 text-gray-300 hover:text-blue-500 cursor-pointer hidden xl:block" role="button"
:class="{'lg:block':!fieldSideBarOpened, 'xl:block':fieldSideBarOpened}"
@click.prevent="openAddFieldSidebar"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</div>
<div class="p-2 text-gray-300 hover:text-blue-500 cursor-pointer" role="button"
:class="{'lg:-mr-2':!fieldSideBarOpened, 'xl:-mr-2':fieldSideBarOpened}"
@click.prevent="editFieldOptions" @click.prevent="editFieldOptions"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
@ -20,7 +30,8 @@
</svg> </svg>
</div> </div>
<div <div
class="px-2 lg:pl-0 lg:pr-1 lg:pt-2 pb-2 bg-white rounded-md text-gray-300 hover:text-gray-500 cursor-grab draggable" class="px-2 xl:pl-0 lg:pr-1 lg:pt-2 pb-2 bg-white rounded-md text-gray-300 hover:text-gray-500 cursor-grab draggable"
:class="{'lg:pr-1 lg:pl-0':!fieldSideBarOpened, 'xl:-mr-2':fieldSideBarOpened}"
role="button" role="button"
> >
<svg <svg
@ -38,23 +49,23 @@
</div> </div>
</div> </div>
<component :is="getFieldComponents" v-if="getFieldComponents" <component :is="getFieldComponents" v-if="getFieldComponents"
v-bind="inputProperties(field)" :required="isFieldRequired" v-bind="inputProperties(field)" :required="isFieldRequired"
:disabled="isFieldDisabled" :disabled="isFieldDisabled"
/> />
<template v-else> <template v-else>
<div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id" <div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id"
class="nf-text w-full px-2 mb-3" :class="[getFieldAlignClasses(field)]" class="nf-text w-full px-2 mb-3" :class="[getFieldAlignClasses(field)]"
v-html="field.content" v-html="field.content"
/> />
<div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id" <div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id"
class="nf-code w-full px-2 mb-3" class="nf-code w-full px-2 mb-3"
v-html="field.content" v-html="field.content"
/> />
<div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id" <div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id"
class="border-b my-4 w-full mx-2" class="border-b my-4 w-full mx-2"
/> />
<div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id" <div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id"
:key="field.id" class="my-4 w-full px-2" :class="[getFieldAlignClasses(field)]" :key="field.id" class="my-4 w-full px-2" :class="[getFieldAlignClasses(field)]"
> >
<div v-if="!field.image_block" class="p-4 border border-dashed"> <div v-if="!field.image_block" class="p-4 border border-dashed">
Open <b>{{ field.name }}'s</b> block settings to upload image. Open <b>{{ field.name }}'s</b> block settings to upload image.
@ -100,9 +111,9 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview adminPreview: {type: Boolean, default: false} // If used in FormEditorPreview
}, },
data () { data() {
return {} return {}
}, },
@ -128,7 +139,7 @@ export default {
/** /**
* Get the right input component for the field/options combination * Get the right input component for the field/options combination
*/ */
getFieldComponents() { getFieldComponents() {
const field = this.field const field = this.field
if (field.type === 'text' && field.multi_lines) { if (field.type === 'text' && field.multi_lines) {
return 'TextAreaInput' return 'TextAreaInput'
@ -150,27 +161,27 @@ export default {
} }
return this.fieldComponents[field.type] return this.fieldComponents[field.type]
}, },
isPublicFormPage () { isPublicFormPage() {
return this.$route.name === 'forms.show_public' return this.$route.name === 'forms.show_public'
}, },
isFieldHidden () { isFieldHidden() {
return !this.showHidden && this.shouldBeHidden return !this.showHidden && this.shouldBeHidden
}, },
shouldBeHidden () { shouldBeHidden() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isHidden() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isHidden()
}, },
isFieldRequired () { isFieldRequired() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isRequired() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isRequired()
}, },
isFieldDisabled () { isFieldDisabled() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled()
}, },
beingEdited() { beingEdited() {
return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item)=>{ return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return item.id === this.field.id return item.id === this.field.id
}) === this.selectedFieldIndex }) === this.selectedFieldIndex
}, },
selectionFieldsOptions () { selectionFieldsOptions() {
// For auto update hidden options // For auto update hidden options
let fieldsOptions = [] let fieldsOptions = []
@ -184,21 +195,28 @@ export default {
} }
return fieldsOptions return fieldsOptions
},
fieldSideBarOpened() {
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
} }
}, },
watch: {}, watch: {},
mounted () {}, mounted() {
},
methods: { methods: {
editFieldOptions () { editFieldOptions() {
this.$store.commit('open/working_form/openSettingsForField', this.field) this.$store.commit('open/working_form/openSettingsForField', this.field)
}, },
openAddFieldSidebar() {
this.$store.commit('open/working_form/openAddFieldSidebar', this.field)
},
/** /**
* Get the right input component for the field/options combination * Get the right input component for the field/options combination
*/ */
getFieldClasses () { getFieldClasses() {
let classes = '' let classes = ''
if (this.adminPreview) { if (this.adminPreview) {
classes += '-mx-4 px-4 -my-1 py-1 group/nffield relative transition-colors' classes += '-mx-4 px-4 -my-1 py-1 group/nffield relative transition-colors'
@ -209,7 +227,7 @@ export default {
} }
return classes return classes
}, },
getFieldWidthClasses (field) { getFieldWidthClasses(field) {
if (!field.width || field.width === 'full') return 'w-full px-2' if (!field.width || field.width === 'full') return 'w-full px-2'
else if (field.width === '1/2') { else if (field.width === '1/2') {
return 'w-full sm:w-1/2 px-2' return 'w-full sm:w-1/2 px-2'
@ -223,7 +241,7 @@ export default {
return 'w-full sm:w-3/4 px-2' return 'w-full sm:w-3/4 px-2'
} }
}, },
getFieldAlignClasses (field) { getFieldAlignClasses(field) {
if (!field.align || field.align === 'left') return 'text-left' if (!field.align || field.align === 'left') return 'text-left'
else if (field.align === 'right') { else if (field.align === 'right') {
return 'text-right' return 'text-right'

View File

@ -69,6 +69,7 @@
<form-editor-preview/> <form-editor-preview/>
<form-field-edit-sidebar/> <form-field-edit-sidebar/>
<add-form-block-sidebar/>
<!-- Form Error Modal --> <!-- Form Error Modal -->
<form-error-modal <form-error-modal
@ -85,6 +86,7 @@
<script> <script>
import {mapGetters} from 'vuex' import {mapGetters} from 'vuex'
import AddFormBlockSidebar from './form-components/AddFormBlockSidebar.vue'
import FormFieldEditSidebar from '../fields/FormFieldEditSidebar.vue' import FormFieldEditSidebar from '../fields/FormFieldEditSidebar.vue'
import FormErrorModal from './form-components/FormErrorModal.vue' import FormErrorModal from './form-components/FormErrorModal.vue'
import FormInformation from './form-components/FormInformation.vue' import FormInformation from './form-components/FormInformation.vue'
@ -103,6 +105,7 @@ import fieldsLogic from '../../../../mixins/forms/fieldsLogic.js'
export default { export default {
name: 'FormEditor', name: 'FormEditor',
components: { components: {
AddFormBlockSidebar,
FormFieldEditSidebar, FormFieldEditSidebar,
FormEditorPreview, FormEditorPreview,
FormIntegrations, FormIntegrations,

View File

@ -1,8 +1,15 @@
<template> <template>
<div> <div>
<add-form-block-modal :form-blocks="formFields" :show="showAddBlock" @block-added="blockAdded" <v-button v-if="formFields && formFields.length > 8"
@close="showAddBlock=false" class="w-full mb-3" color="light-gray"
/> @click="openAddFieldSidebar">
<svg class="w-4 h-4 text-nt-blue inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M7.00001 1.1665V12.8332M1.16667 6.99984H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Add block
</v-button>
<draggable v-model="formFields" <draggable v-model="formFields"
class="bg-white overflow-hidden dark:bg-notion-dark-light rounded-md w-full mx-auto border transition-colors" class="bg-white overflow-hidden dark:bg-notion-dark-light rounded-md w-full mx-auto border transition-colors"
@ -124,8 +131,8 @@
</draggable> </draggable>
<v-button <v-button
class="w-full mt-4" color="light-gray" class="w-full mt-3" color="light-gray"
@click="showAddBlock=true"> @click="openAddFieldSidebar">
<svg class="w-4 h-4 text-nt-blue inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" <svg class="w-4 h-4 text-nt-blue inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path d="M7.00001 1.1665V12.8332M1.16667 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" <path d="M7.00001 1.1665V12.8332M1.16667 6.99984H12.8333" stroke="currentColor" stroke-width="1.67"
@ -139,7 +146,6 @@
<script> <script>
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import AddFormBlockModal from './form-components/AddFormBlockModal.vue'
import ProTag from '../../../common/ProTag.vue' import ProTag from '../../../common/ProTag.vue'
import clonedeep from 'clone-deep' import clonedeep from 'clone-deep'
import EditableDiv from '../../../common/EditableDiv.vue' import EditableDiv from '../../../common/EditableDiv.vue'
@ -150,7 +156,6 @@ export default {
components: { components: {
VButton, VButton,
ProTag, ProTag,
AddFormBlockModal,
draggable, draggable,
EditableDiv EditableDiv
}, },
@ -158,7 +163,6 @@ export default {
data() { data() {
return { return {
formFields: [], formFields: [],
showAddBlock: false,
removing: null removing: null
} }
}, },
@ -172,7 +176,7 @@ export default {
set(value) { set(value) {
this.$store.commit('open/working_form/set', value) this.$store.commit('open/working_form/set', value)
} }
}, }
}, },
watch: { watch: {
@ -289,10 +293,6 @@ export default {
editOptions(index) { editOptions(index) {
this.$store.commit('open/working_form/openSettingsForField', index) this.$store.commit('open/working_form/openSettingsForField', index)
}, },
blockAdded(block) {
this.formFields.push(block)
this.$store.commit('open/working_form/openSettingsForField', this.formFields.length-1)
},
removeBlock(blockIndex) { removeBlock(blockIndex) {
const newFields = clonedeep(this.formFields) const newFields = clonedeep(this.formFields)
newFields.splice(blockIndex, 1) newFields.splice(blockIndex, 1)
@ -301,6 +301,9 @@ export default {
}, },
closeSidebar() { closeSidebar() {
this.$store.commit('open/working_form/closeEditFieldSidebar') this.$store.commit('open/working_form/closeEditFieldSidebar')
},
openAddFieldSidebar() {
this.$store.commit('open/working_form/openAddFieldSidebar', null)
} }
} }
} }

View File

@ -1,336 +0,0 @@
<template>
<modal :show="show" @close="close">
<p class="text-gray-500 uppercase text-xs font-semibold mb-2">Input Blocks</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Text Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('text')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Text Input</p>
</div>
<!-- Date Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('date')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Date Input</p>
</div>
<!-- Url Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('url')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">URL Input</p>
</div>
<!-- Phone Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('phone_number')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Phone Input</p>
</div>
<!-- email Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('email')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Email Input</p>
</div>
<!-- checkbox Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('checkbox')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Checkbox Input</p>
</div>
<!-- select Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('select')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Select Input</p>
</div>
<!-- multiselect Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('multi_select')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold">Multi-select Input</p>
</div>
<!-- number Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('number')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Number Input</p>
</div>
<!-- files Input -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('files')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">File Input</p>
</div>
<!-- Signature Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('signature')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Signature Input</p>
</div>
</div>
<p class="text-gray-500 uppercase text-xs font-semibold mb-2 mt-6">Layout Blocks</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Text Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-text')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Text Block</p>
</div>
<!-- Page Break Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-page-break')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold">Page-break Block</p>
</div>
<!-- Divider Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-divider')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Divider block</p>
</div>
<!-- Image Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-image')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Image Block</p>
</div>
<!-- Code Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-code')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Code Block</p>
</div>
</div>
</modal>
</template>
<script>
import Form from 'vform'
import VButton from '../../../../common/Button.vue'
export default {
name: 'AddFormBlockModal',
components: {VButton},
props: {
formBlocks: {
type: Array,
required: true
},
show: {
type: Boolean,
required: true
}
},
data() {
return {
blockForm: null
}
},
computed: {
defaultBlockNames() {
return {
'text': 'Your name',
'date': 'Date',
'url': 'Link',
'phone_number': 'Phone Number',
'number': 'Number',
'email': 'Email',
'checkbox': 'Checkbox',
'select': 'Select',
'multi_select': 'Multi Select',
'files': 'Files',
'signature': 'Signature',
'nf-text': 'Text Block',
'nf-page-break': 'Page Break',
'nf-divider': 'Divider',
'nf-image': 'Image',
'nf-code': 'Code Block',
}
}
},
watch: {},
mounted() {
this.reset()
},
methods: {
reset() {
this.blockForm = new Form({
type: null,
name: null
})
},
addBlock(type) {
this.blockForm.type = type
this.blockForm.name = this.defaultBlockNames[type]
const data = this.prefillDefault(this.blockForm.data())
data.id = this.generateUUID()
data.hidden = false
if (['select', 'multi_select'].includes(this.blockForm.type)) {
data[this.blockForm.type] = {'options': []}
}
data.help_position = 'below_input'
this.$emit('block-added', data)
this.close()
},
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)
})
},
prefillDefault(data) {
if (data.type === 'nf-text') {
data.content = '<p>This is a text block.</p>'
} else if (data.type === 'nf-page-break') {
data.next_btn_text = 'Next'
data.previous_btn_text = 'Previous'
} else if (data.type === 'nf-code') {
data.content = '<div class="text-blue-500 italic">This is a code block.</div>'
} else if (data.type === 'signature') {
data.help = 'Draw your signature above'
}
return data
},
close() {
this.$emit('close')
this.reset()
}
}
}
</script>

View File

@ -0,0 +1,262 @@
<template>
<div v-if="showSidebar"
class="absolute shadow-lg shadow-blue-800/30 top-0 h-[calc(100vh-45px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll md:max-w-[20rem] flex-shrink-0">
<div class="p-4 border-b sticky top-0 z-10 bg-white">
<div class="flex">
<button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="closeSidebar">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</button>
<div class="font-semibold inline ml-2 truncate flex-grow truncate">
Add Block
</div>
</div>
</div>
<div class="py-2 px-4">
<div>
<p class="text-gray-500 uppercase text-xs font-semibold mb-2">Input Blocks</p>
<div class="grid grid-cols-2 gap-2">
<div v-for="(block, i) in inputBlocks" :key="block.name"
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 py-2 flex flex-col"
role="button" @click.prevent="addBlock(block.name)"
>
<div class="mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2" v-html="block.icon"></svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1">{{ block.title }}</p>
</div>
</div>
</div>
<div class="border-t mt-6">
<p class="text-gray-500 uppercase text-xs font-semibold mb-2 mt-6">Layout Blocks</p>
<div class="grid grid-cols-2 gap-2">
<div v-for="(block, i) in layoutBlocks" :key="block.name"
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 py-2 flex flex-col"
role="button" @click.prevent="addBlock(block.name)"
>
<div class="mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2" v-html="block.icon"></svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1">{{ block.title }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import Form from 'vform'
import clonedeep from 'clone-deep'
export default {
name: 'AddFormBlockSidebar',
components: {},
props: {},
data() {
return {
blockForm: null,
inputBlocks: [
{
name: 'text',
title: 'Text Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7"/>',
},
{
name: 'date',
title: 'Date Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
},
{
name: 'url',
title: 'URL Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>',
},
{
name: 'phone_number',
title: 'Phone Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>',
},
{
name: 'email',
title: 'Email Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>',
},
{
name: 'checkbox',
title: 'Checkbox Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
},
{
name: 'select',
title: 'Select Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>',
},
{
name: 'multi_select',
title: 'Multi-select Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>',
},
{
name: 'number',
title: 'Number Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>',
},
{
name: 'files',
title: 'File Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />',
},
{
name: 'signature',
title: 'Signature Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />',
}
],
layoutBlocks: [
{
name: 'nf-text',
title: 'Text Block',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h8m-8 6h16" />',
},
{
name: 'nf-page-break',
title: 'Page-break Block',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />',
},
{
name: 'nf-divider',
title: 'Divider Block',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />',
},
{
name: 'nf-image',
title: 'Image Block',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />',
},
{
name: 'nf-code',
title: 'Code Block',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />',
}
]
}
},
computed: {
...mapState({
selectedFieldIndex: state => state['open/working_form'].selectedFieldIndex,
showAddFieldSidebar: state => state['open/working_form'].showAddFieldSidebar
}),
form: {
get() {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set(value) {
this.$store.commit('open/working_form/set', value)
}
},
showSidebar() {
return (this.form && this.showAddFieldSidebar) ?? false
},
defaultBlockNames() {
return {
'text': 'Your name',
'date': 'Date',
'url': 'Link',
'phone_number': 'Phone Number',
'number': 'Number',
'email': 'Email',
'checkbox': 'Checkbox',
'select': 'Select',
'multi_select': 'Multi Select',
'files': 'Files',
'signature': 'Signature',
'nf-text': 'Text Block',
'nf-page-break': 'Page Break',
'nf-divider': 'Divider',
'nf-image': 'Image',
'nf-code': 'Code Block',
}
}
},
watch: {},
mounted() {
this.reset()
},
methods: {
closeSidebar() {
this.$store.commit('open/working_form/closeAddFieldSidebar')
},
reset() {
this.blockForm = new Form({
type: null,
name: null
})
},
addBlock(type) {
this.blockForm.type = type
this.blockForm.name = this.defaultBlockNames[type]
const newBlock = this.prefillDefault(this.blockForm.data())
newBlock.id = this.generateUUID()
newBlock.hidden = false
if (['select', 'multi_select'].includes(this.blockForm.type)) {
newBlock[this.blockForm.type] = {'options': []}
}
newBlock.help_position = 'below_input'
if(this.selectedFieldIndex === null || this.selectedFieldIndex === undefined){
const newFields = clonedeep(this.form.properties)
newFields.push(newBlock)
this.$set(this.form, 'properties', newFields)
this.$store.commit('open/working_form/openSettingsForField', this.form.properties.length-1)
} else {
const newFields = clonedeep(this.form.properties)
newFields.splice(this.selectedFieldIndex+1, 0, newBlock)
this.$set(this.form, 'properties', newFields)
this.$store.commit('open/working_form/openSettingsForField', this.selectedFieldIndex+1)
}
this.reset()
},
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)
})
},
prefillDefault(data) {
if (data.type === 'nf-text') {
data.content = '<p>This is a text block.</p>'
} else if (data.type === 'nf-page-break') {
data.next_btn_text = 'Next'
data.previous_btn_text = 'Previous'
} else if (data.type === 'nf-code') {
data.content = '<div class="text-blue-500 italic">This is a code block.</div>'
} else if (data.type === 'signature') {
data.help = 'Draw your signature above'
}
return data
}
}
}
</script>

View File

@ -6,7 +6,8 @@ export const state = {
// Field being edited // Field being edited
selectedFieldIndex: null, selectedFieldIndex: null,
showEditFieldSidebar: null showEditFieldSidebar: null,
showAddFieldSidebar: null
} }
// mutations // mutations
@ -24,12 +25,25 @@ export const mutations = {
} }
state.selectedFieldIndex = index state.selectedFieldIndex = index
state.showEditFieldSidebar = true state.showEditFieldSidebar = true
}, state.showAddFieldSidebar = false
setSelectedFieldIndex (state, index) { },
state.selectedFieldIndex = index
},
closeEditFieldSidebar (state) { closeEditFieldSidebar (state) {
state.showEditFieldSidebar = false
state.selectedFieldIndex = null state.selectedFieldIndex = null
} state.showEditFieldSidebar = false
state.showAddFieldSidebar = false
},
openAddFieldSidebar (state, index) {
// If field is passed, compute index
if (index !== null && typeof index === 'object') {
index = state.content.properties.findIndex(prop => prop.id === index.id)
}
state.selectedFieldIndex = index
state.showAddFieldSidebar = true
state.showEditFieldSidebar = false
},
closeAddFieldSidebar (state) {
state.selectedFieldIndex = null
state.showAddFieldSidebar = false
state.showEditFieldSidebar = false
},
} }