group form checkboxes into single step

pull/105/head
Alex Turchyn 2 years ago
parent 7978b3ebe8
commit bfbf3d7ddb

@ -1,11 +1,11 @@
<template>
<div
class="flex cursor-pointer bg-red-100 absolute border"
class="flex cursor-pointer bg-red-100 absolute border text-[1.5vw] lg:text-base"
:style="computedStyle"
:class="{ 'border-red-100 bg-opacity-70': !isActive, 'border-red-500 border-dashed bg-opacity-30 z-10': isActive }"
:class="{ 'border-red-100': !isActive, 'bg-opacity-70': !isActive && !isValue, 'border-red-500 border-dashed z-10': isActive, 'bg-opacity-30': isActive || isValue }"
>
<div
v-if="!isActive && !modelValue"
v-if="!isActive && !isValue && field.type !== 'checkbox'"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
>
<span
@ -25,6 +25,9 @@
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
>
{{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox'">
{{ fieldIndex + 1 }}
</template>
</div>
<div
v-if="isActive"
@ -33,39 +36,83 @@
/>
<img
v-if="field.type === 'image' && image"
class="object-contain"
class="object-contain mx-auto"
:src="image.url"
>
<img
v-else-if="field.type === 'signature' && signature"
class="object-contain"
class="object-contain mx-auto"
:src="signature.url"
>
<div v-else-if="field.type === 'attachment'">
<div
v-else-if="field.type === 'file'"
class="px-0.5 flex items-center"
>
<a
v-for="(attachment, index) in attachments"
:key="index"
target="_blank"
:href="attachment.url"
>
<IconPaperclip
class="inline w-[1.5vw] h-[1.5vw] lg:w-4 lg:h-4"
/>
{{ attachment.filename }}
</a>
</div>
<div
v-else-if="field.type === 'checkbox'"
class="w-full p-[0.2vw] flex items-center justify-center"
>
<input
type="checkbox"
:value="false"
class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue"
@click="$emit('update:model-value', !modelValue)"
>
</div>
<div
v-else
class="flex items-center px-0.5"
>
<span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }}
</span>
<span v-else-if="field.type === 'date'">
{{ formattedDate }}
</span>
<span v-else>
{{ modelValue }}
</span>
</div>
</div>
</template>
<script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
components: {
IconPaperclip
},
props: {
field: {
type: Object,
required: true
},
step: {
type: Array,
required: false,
default: () => []
},
values: {
type: Object,
required: false,
default: () => ({})
},
modelValue: {
type: [Array, String, Number, Object, Boolean],
required: false,
@ -76,6 +123,11 @@ export default {
required: false,
default: false
},
fieldIndex: {
type: Number,
required: false,
default: 0
},
attachmentsIndex: {
type: Object,
required: false,
@ -97,9 +149,13 @@ export default {
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
radio: 'Radio'
radio: 'Radio',
multiple: 'Multiple Select'
}
},
isValue () {
return this.step.some((f) => ![null, undefined, ''].includes(this.values[f.uuid]))
},
fieldIcons () {
return {
text: IconTextSize,
@ -109,7 +165,8 @@ export default {
file: IconPaperclip,
select: IconSelect,
checkbox: IconCheckbox,
radio: IconCircleDot
radio: IconCircleDot,
multiple: IconChecks
}
},
image () {
@ -126,8 +183,15 @@ export default {
return null
}
},
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return new Intl.DateTimeFormat({ year: 'numeric', month: 'numeric', day: 'numeric' }).format(new Date(this.modelValue))
} else {
return ''
}
},
attachments () {
if (this.field.type === 'attachment') {
if (this.field.type === 'file') {
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
} else {
return []

@ -1,26 +1,34 @@
<template>
<template
v-for="field in fields"
v-for="step in steps"
:key="step[0].uuid"
>
<template
v-for="(field, fieldIndex) in step"
:key="field.uuid"
>
<template
v-for="(area, index) in field.areas"
:key="index"
v-for="(area, areaIndex) in field.areas"
:key="areaIndex"
>
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
:field="field"
:values="values"
:area="area"
:is-active="currentField === field"
:field-index="fieldIndex"
:step="step"
:is-active="currentStep === step"
:attachments-index="attachmentsIndex"
@click="$emit('focus-field', field)"
@click="$emit('focus-step', step)"
/>
</Teleport>
</template>
</template>
</template>
</template>
<script>
import FieldArea from './area'
@ -31,7 +39,7 @@ export default {
FieldArea
},
props: {
fields: {
steps: {
type: Array,
required: false,
default: () => []
@ -46,13 +54,13 @@ export default {
required: false,
default: () => ({})
},
currentField: {
type: Object,
currentStep: {
type: Array,
required: false,
default: () => ({})
default: () => []
}
},
emits: ['focus-field'],
emits: ['focus-step'],
data () {
return {
areaRefs: []

@ -1,11 +1,11 @@
<template>
<FieldAreas
ref="areas"
:fields="submitterFields"
:steps="stepFields"
:values="values"
:attachments-index="attachmentsIndex"
:current-field="currentField"
@focus-field="goToField($event, false, true)"
:current-step="currentStepFields"
@focus-step="goToStep($event, false, true)"
/>
<form
v-if="!isCompleted"
@ -21,7 +21,7 @@
:value="authenticityToken"
>
<input
v-if="currentStep === submitterFields.length - 1"
v-if="currentStep === stepFields.length - 1"
type="hidden"
name="completed"
value="true"
@ -42,12 +42,12 @@
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
autofocus
class="base-input !text-2xl w-full"
:required="currentField.required"
placeholder="Type here..."
type="text"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
@ -57,15 +57,15 @@
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div>
<div class="text-center">
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full text-center"
autofocus
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
@ -81,6 +81,7 @@
class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)"
>
<option
value=""
@ -98,13 +99,14 @@
</option>
</select>
</div>
<div v-else-if="currentField.type === 'radio' && currentField.options?.length">
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="space-y-3.5">
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
@ -129,34 +131,43 @@
</div>
</div>
</div>
<CheckboxStep
v-else-if="currentField.type === 'checkbox' && currentField.options?.length"
</div>
<MultiSelectStep
v-else-if="currentField.type === 'multiple'"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div v-else-if="['radio', 'checkbox'].includes(currentField.type)">
<div class="flex justify-center">
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<div
class="space-y-3.5 mx-auto"
>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="currentField.uuid"
:for="field.uuid"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid"
:model-value="values[currentField.uuid]"
:type="currentField.type"
:name="`values[${currentField.uuid}]`"
:value="true"
class="!h-7 !w-7"
:class="{'base-radio' : currentField.type === 'radio', 'base-checkbox': currentField.type === 'checkbox'}"
:checked="!!values[currentField.uuid]"
@click="values[currentField.uuid] = !values[currentField.uuid]"
:id="field.uuid"
type="checkbox"
:name="`values[${field.uuid}]`"
:value="false"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="values[field.uuid] = !values[field.uuid]"
>
<span class="text-xl">
{{ currentField.name || currentField.type }}
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</div>
</div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
@ -199,12 +210,12 @@
<div class="flex justify-center">
<div class="flex items-center mt-5 mb-1">
<a
v-for="(field, index) in submitterFields"
:key="field.uuid"
v-for="(step, index) in stepFields"
:key="step[0].uuid"
href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep, 'bg-white': index > currentStep }"
@click.prevent="goToField(field, true)"
@click.prevent="goToStep(step, true)"
/>
</div>
</div>
@ -220,7 +231,7 @@ import FieldAreas from './areas'
import ImageStep from './image_step'
import SignatureStep from './signature_step'
import AttachmentStep from './attachment_step'
import CheckboxStep from './checkbox_step'
import MultiSelectStep from './multi_select_step'
import FormCompleted from './completed'
export default {
@ -230,7 +241,7 @@ export default {
ImageStep,
SignatureStep,
AttachmentStep,
CheckboxStep,
MultiSelectStep,
FormCompleted
},
props: {
@ -270,12 +281,28 @@ export default {
}
},
computed: {
currentStepFields () {
return this.stepFields[this.currentStep]
},
currentField () {
return this.submitterFields[this.currentStep]
return this.currentStepFields[0]
},
submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid)
},
stepFields () {
return this.submitterFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1]
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
prevStep.push(f)
} else {
acc.push([f])
}
return acc
}, [])
},
attachmentsIndex () {
return this.attachments.reduce((acc, a) => {
acc[a.uuid] = a
@ -289,17 +316,17 @@ export default {
},
mounted () {
this.currentStep = Math.min(
this.submitterFields.indexOf([...this.submitterFields].reverse().find((field) => !!this.values[field.uuid])) + 1,
this.submitterFields.length - 1
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
this.stepFields.length - 1
)
},
methods: {
goToField (field, scrollToArea = false, clickUpload = false) {
this.currentStep = this.submitterFields.indexOf(field)
goToStep (step, scrollToArea = false, clickUpload = false) {
this.currentStep = this.stepFields.indexOf(step)
this.$nextTick(() => {
if (scrollToArea) {
this.$refs.areas.scrollIntoField(field)
this.$refs.areas.scrollIntoField(step[0])
}
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
@ -322,10 +349,10 @@ export default {
method: 'POST',
body: new FormData(this.$refs.form)
}).then(response => {
const nextField = this.submitterFields[this.currentStep + 1]
const nextStep = this.stepFields[this.currentStep + 1]
if (nextField) {
this.goToField(this.submitterFields[this.currentStep + 1], true)
if (nextStep) {
this.goToStep(this.stepFields[this.currentStep + 1], true)
} else {
this.isCompleted = true
}

@ -4,7 +4,8 @@
:for="field.uuid"
class="label text-2xl mb-2"
>{{ field.name }}</label>
<div class="space-y-3.5">
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in field.options"
:key="index"
@ -29,11 +30,12 @@
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SheckboxStep',
name: 'MultiSelectStep',
props: {
field: {
type: Object,

@ -35,7 +35,7 @@
@click="selectedAreaRef.value = area"
/>
<span
v-if="withName"
v-if="field.type !== 'checkbox' || field.name"
ref="name"
contenteditable
class="pr-1 cursor-text outline-none block"
@ -138,9 +138,6 @@ export default {
'bg-purple-100'
]
},
withName () {
return !['checkbox', 'radio'].includes(this.field.type) || this.field.options
},
isSelected () {
return this.selectedAreaRef.value === this.area
},
@ -169,11 +166,11 @@ export default {
}
},
maybeUpdateOptions () {
if (!['radio', 'checkbox', 'select'].includes(this.field.type)) {
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options
}
if (this.field.type === 'select') {
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
this.field.options ||= ['']
}
},

@ -291,7 +291,7 @@ export default {
required: true
}
if (this.dragFieldType === 'select') {
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
field.options = ['']
}
@ -311,7 +311,7 @@ export default {
} else if (previousField?.areas) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
if (['checkbox', 'radio'].includes(this.dragFieldType)) {
if (['checkbox'].includes(this.dragFieldType)) {
baseArea = {
w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)

@ -182,7 +182,9 @@ export default {
defaultName () {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
return `${this.$t(this.field.type)} Field ${typeIndex + 1}`
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
return `${this.$t(this.field.type)} ${suffix} ${typeIndex + 1}`
},
areas () {
return this.field.areas || []
@ -203,11 +205,11 @@ export default {
document.activeElement.blur()
},
maybeUpdateOptions () {
if (!['radio', 'checkbox', 'select'].includes(this.field.type)) {
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options
}
if (this.field.type === 'select') {
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
this.field.options ||= ['']
}
},

@ -41,7 +41,7 @@
</template>
<script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
@ -77,6 +77,7 @@ export default {
file: IconPaperclip,
select: IconSelect,
checkbox: IconCheckbox,
multiple: IconChecks,
radio: IconCircleDot
}
}

@ -1,11 +1,13 @@
<template>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<FieldSubmitter
:model-value="selectedSubmitter.uuid"
class="w-full"
class="w-full bg-base-100"
:submitters="submitters"
@remove="removeSubmitter"
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
/>
</div>
<div class="mb-1 mt-2">
<Field
v-for="field in submitterFields"
@ -19,7 +21,7 @@
@set-draw="$emit('set-draw', $event)"
/>
</div>
<div class="grid grid-cols-3 gap-1">
<div class="grid grid-cols-3 gap-1 pb-2">
<button
v-for="(icon, type) in fieldIcons"
:key="type"
@ -145,7 +147,7 @@ export default {
type
}
if (['select', 'checkbox', 'radio'].includes(type)) {
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = ['']
}

@ -6,5 +6,6 @@ export default {
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio'
}

@ -18,7 +18,7 @@
<% end %>
<div class="text-center px-2">
Powered by
<a href="https://www.docuseal.co" class="underline">DocuSeal</a> - An open source documents software
<a href="https://www.docuseal.co" class="underline">DocuSeal</a> - open source documents software
</div>
</div>
<div class="fixed bottom-0 w-full h-0 z-20">

Loading…
Cancel
Save