style submitter form

pull/105/head
Alex Turchyn 2 years ago
parent 199ccf082b
commit fef54859bf

@ -31,20 +31,8 @@ button[disabled] .enabled {
display: none;
}
button .disabled {
display: none;
}
button[disabled] .disabled {
display: initial;
}
button .enabled {
display: initial;
}
button[disabled] .enabled {
display: none;
select:required:invalid {
@apply text-gray-300;
}
.btn {
@ -54,6 +42,15 @@ button[disabled] .enabled {
.base-input {
@apply input input-bordered bg-white;
}
.base-button {
@apply btn btn-neutral text-white text-base;
}
.base-checkbox {
@apply checkbox rounded bg-white checkbox-sm;
}
.base-radio {
@apply radio bg-white radio-sm;
}

@ -1,8 +1,36 @@
<template>
<div
class="flex cursor-pointer bg-red-100 bg-opacity-60 absolute"
class="flex cursor-pointer bg-red-100 absolute border"
:style="computedStyle"
:class="{ 'border-red-100 bg-opacity-70': !isActive, 'border-red-500 border-dashed bg-opacity-30 z-10': isActive }"
>
<div
v-if="!isActive && !modelValue"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
>
<span
v-if="field"
class="flex justify-center items-center h-full opacity-50"
>
<component
:is="fieldIcons[field.type]"
width="100%"
height="100%"
class="max-h-10 text-base-content"
/>
</span>
</div>
<div
v-if="isActive"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
>
{{ field.name || fieldNames[field.type] }}
</div>
<div
v-if="isActive"
ref="scrollToElem"
class="absolute -top-20"
/>
<img
v-if="field.type === 'image' && image"
class="object-contain"
@ -23,12 +51,14 @@
</a>
</div>
<span v-else>
{{ value }}
{{ modelValue }}
</span>
</div>
</template>
<script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
props: {
@ -36,11 +66,16 @@ export default {
type: Object,
required: true
},
value: {
type: [Array, String, Number, Object],
modelValue: {
type: [Array, String, Number, Object, Boolean],
required: false,
default: ''
},
isActive: {
type: Boolean,
required: false,
default: false
},
attachmentsIndex: {
type: Object,
required: false,
@ -51,24 +86,49 @@ export default {
required: true
}
},
emits: ['update:model-value'],
computed: {
fieldNames () {
return {
text: 'Text',
signature: 'Signature',
date: 'Date',
image: 'Image',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
radio: 'Radio'
}
},
fieldIcons () {
return {
text: IconTextSize,
signature: IconWriting,
date: IconCalendarEvent,
image: IconPhoto,
file: IconPaperclip,
select: IconSelect,
checkbox: IconCheckbox,
radio: IconCircleDot
}
},
image () {
if (this.field.type === 'image') {
return this.attachmentsIndex[this.value]
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
signature () {
if (this.field.type === 'signature') {
return this.attachmentsIndex[this.value]
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
attachments () {
if (this.field.type === 'attachment') {
return (this.value || []).map((uuid) => this.attachmentsIndex[uuid])
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
} else {
return []
}

@ -10,10 +10,11 @@
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
:field="field"
:area="area"
:is-active="currentField === field"
:attachments-index="attachmentsIndex"
:value="values[field.uuid]"
@click="$emit('focus-field', field)"
/>
</Teleport>
@ -44,6 +45,11 @@ export default {
type: Object,
required: false,
default: () => ({})
},
currentField: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['focus-field'],
@ -59,7 +65,7 @@ export default {
scrollIntoField (field) {
this.areaRefs.find((area) => {
if (area.field === field) {
area.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
return true
} else {

@ -1,29 +1,42 @@
<template>
<div>
<template v-if="modelValue.length">
<div v-if="modelValue.length">
<div
v-for="(val, index) in modelValue"
:key="index"
class="flex mb-2"
>
<input
:value="val"
type="hidden"
:name="`values[${field.uuid}][]`"
>
<a
v-if="val"
class="flex items-center space-x-1.5 w-full"
:href="attachmentsIndex[val].url"
target="_blank"
>
<IconPaperclip
:width="16"
class="flex-none"
:heigh="16"
/>
<span>
{{ attachmentsIndex[val].filename }}
</span>
</a>
<input
:value="val"
type="hidden"
:name="`values[${field.uuid}][]`"
>
<button
v-if="modelValue"
@click.prevent="removeAttachment(val)"
>
Remove
<IconTrashX
:width="18"
:heigh="19"
/>
</button>
</div>
</template>
</div>
<template v-else>
<input
value=""
@ -32,7 +45,7 @@
>
</template>
<FileDropzone
:message="'Attachments'"
:message="`Upload ${field.name || 'Attachments'}`"
:submitter-slug="submitterSlug"
@upload="onUpload"
/>
@ -41,11 +54,14 @@
<script>
import FileDropzone from './dropzone'
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
export default {
name: 'AttachmentStep',
components: {
FileDropzone
FileDropzone,
IconPaperclip,
IconTrashX
},
props: {
field: {

@ -1,22 +1,36 @@
<template>
<label
v-if="field.name"
:for="field.uuid"
class="label text-2xl mb-2"
>{{ field.name }}</label>
<div class="space-y-3.5">
<div
v-for="(option, index) in field.options"
:key="index"
>
<label :for="field.uuid + option">
<label
:for="field.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="field.uuid + option"
:ref="setInputRef"
type="checkbox"
:name="`values[${field.uuid}][]`"
:value="option"
class="base-checkbox !h-7 !w-7"
:checked="modelValue.includes(option)"
@change="onChange"
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</div>
</template>
<script>
export default {
name: 'SheckboxStep',

@ -1,16 +1,30 @@
<template>
<div
class="flex h-20 w-full"
class="flex h-32 w-full"
@dragover.prevent
@drop.prevent="onDropFiles"
>
<label
:for="inputId"
class="w-full"
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center">
<IconCloudUpload
:width="30"
:height="30"
/>
<div
v-if="message"
class="font-medium mb-1"
>
Upload
{{ message }}
</label>
</div>
<div class="text-xs">
<span class="font-medium">Click to upload</span> or drag and drop
</div>
</div>
</div>
<input
:id="inputId"
ref="input"
@ -20,14 +34,19 @@
class="hidden"
@change="onSelectFiles"
>
</label>
</div>
</template>
<script>
import { DirectUpload } from '@rails/activestorage'
import { IconCloudUpload } from '@tabler/icons-vue'
export default {
name: 'FileDropzone',
components: {
IconCloudUpload
},
props: {
message: {
type: String,
@ -62,7 +81,9 @@ export default {
e.preventDefault()
this.uploadFiles(this.$refs.input.files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
}
})
},
async uploadFiles (files) {

@ -4,20 +4,15 @@
:fields="submitterFields"
:values="values"
:attachments-index="attachmentsIndex"
@focus-field="goToField"
:current-field="currentField"
@focus-field="goToField($event, false, true)"
/>
<button
v-if="currentStep !== 0"
@click="goToField(submitterFields[currentStep - 1], true)"
>
Back
</button>
{{ currentField.type }}
<form
v-if="!isCompleted"
ref="form"
:action="submitPath"
method="post"
class="md:mx-16"
@submit.prevent="submitStep"
>
<input
@ -36,46 +31,59 @@
name="_method"
type="hidden"
>
<div>
<template v-if="currentField.type === 'text'">
<label :for="currentField.uuid">{{ currentField.name || 'Text' }}</label>
<div class="mt-4">
<div v-if="currentField.type === 'text'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
autofocus
class="text-xl"
class="base-input !text-2xl w-full"
:required="currentField.required"
placeholder="Type here..."
type="text"
:name="`values[${currentField.uuid}]`"
>
</div>
</template>
<template v-else-if="currentField.type === 'date'">
<label :for="currentField.uuid">{{ currentField.name || 'Date' }}</label>
</div>
<div v-else-if="currentField.type === 'date'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="text-xl"
class="base-input !text-2xl w-full text-center"
autofocus
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
>
</div>
</template>
<template v-else-if="currentField.type === 'select'">
<label :for="currentField.uuid">{{ currentField.name || 'Date' }}</label>
</div>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<select
:id="currentField.uuid"
v-model="values[currentField.uuid]"
:required="currentField.required"
:required="true"
class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
>
<option
value=""
disabled
:selected="!values[currentField.uuid]"
>
Select your option
@ -83,35 +91,72 @@
<option
v-for="(option, index) in currentField.options"
:key="index"
:select="values[currentField.uuid] == option"
:selected="values[currentField.uuid] == option"
:value="option"
>
{{ option }}
</option>
</select>
</template>
<template v-else-if="currentField.type === 'radio'">
</div>
<div v-else-if="currentField.type === 'radio' && currentField.options?.length">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="space-y-3.5">
<div
v-for="(option, index) in currentField.options"
:key="index"
>
<label :for="currentField.uuid + option">
<label
:for="currentField.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
required
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</template>
</div>
</div>
<CheckboxStep
v-else-if="currentField.type === 'checkbox'"
v-else-if="currentField.type === 'checkbox' && currentField.options?.length"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div v-else-if="['radio', 'checkbox'].includes(currentField.type)">
<div class="flex justify-center">
<label
:for="currentField.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]"
>
<span class="text-xl">
{{ currentField.name || currentField.type }}
</span>
</label>
</div>
</div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
@ -138,8 +183,11 @@
@attached="attachments.push($event)"
/>
</div>
<div>
<button type="submit">
<div class="mt-8">
<button
type="submit"
class="base-button w-full"
>
<span v-if="isSubmitting">
Submitting...
</span>
@ -148,6 +196,18 @@
</span>
</button>
</div>
<div class="flex justify-center">
<div class="flex items-center mt-5 mb-1">
<a
v-for="(field, index) in submitterFields"
:key="field.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)"
/>
</div>
</div>
</form>
<FormCompleted
v-else
@ -234,7 +294,7 @@ export default {
)
},
methods: {
goToField (field, scrollToArea = false) {
goToField (field, scrollToArea = false, clickUpload = false) {
this.currentStep = this.submitterFields.indexOf(field)
this.$nextTick(() => {
@ -243,6 +303,10 @@ export default {
}
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
if (clickUpload && !this.values[this.currentField.uuid]) {
this.$refs.form.querySelector('input[type="file"]')?.click()
}
})
},
async submitStep () {

@ -1,23 +1,33 @@
<template>
<div>
<img
v-if="modelValue"
class="w-80"
:src="attachmentsIndex[modelValue].url"
>
<div v-if="modelValue">
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || 'Image' }}</label>
<button
v-if="modelValue"
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
Remove
<IconReload :width="16" />
Reupload
</button>
</div>
<div>
<img
:src="attachmentsIndex[modelValue].url"
class="h-52 border border-base-300 rounded mx-auto"
>
</div>
<input
:value="modelValue"
type="hidden"
:name="`values[${field.uuid}]`"
>
</div>
<div>
<FileDropzone
:message="'Image'"
v-if="!modelValue"
:message="`Upload ${field.name || 'Image'}`"
:submitter-slug="submitterSlug"
:accept="'image/*'"
@upload="onImageUpload"
@ -27,10 +37,13 @@
<script>
import FileDropzone from './dropzone'
import { IconReload } from '@tabler/icons-vue'
export default {
name: 'ImageStep',
components: {
FileDropzone
FileDropzone,
IconReload
},
props: {
field: {

@ -1,5 +1,26 @@
<template>
<div>
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || 'Signature' }}</label>
<button
v-if="modelValue"
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
Redraw
</button>
<button
v-else
class="btn btn-outline btn-sm"
@click.prevent="clear"
>
<IconReload :width="16" />
Clear
</button>
</div>
<input
:value="modelValue"
type="hidden"
@ -8,32 +29,26 @@
<img
v-if="modelValue"
:src="attachmentsIndex[modelValue].url"
class="w-full bg-white border border-base-300 rounded"
>
<canvas
v-show="!modelValue"
ref="canvas"
class="bg-white border border-base-300 rounded"
/>
<button
v-if="modelValue"
@click.prevent="remove"
>
Redraw
</button>
<button
v-else
@click.prevent="clear"
>
Clear
</button>
</div>
</template>
<script>
import SignaturePad from 'signature_pad'
import { DirectUpload } from '@rails/activestorage'
import { IconReload } from '@tabler/icons-vue'
export default {
name: 'SignatureStep',
components: {
IconReload
},
props: {
field: {
type: Object,
@ -56,6 +71,9 @@ export default {
},
emits: ['attached', 'update:model-value'],
mounted () {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3
this.pad = new SignaturePad(this.$refs.canvas)
},
methods: {

@ -291,6 +291,10 @@ export default {
required: true
}
if (this.dragFieldType === 'select') {
field.options = ['']
}
const fieldArea = {
x: (area.x - 6) / area.maskW,
y: area.y / area.maskH,

@ -12,8 +12,6 @@
<%= yield :head %>
</head>
<body class="h-full">
<div class="max-w-6xl mx-auto px-4 md:px-2">
<%= yield %>
</div>
</body>
</html>

@ -1,4 +1,4 @@
<svg height="<%= local_assigns.fetch(:height, '37px') %>" width="<%= local_assigns.fetch(:width, '37px') %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.998 511.998" xml:space="preserve">
<svg height="<%= local_assigns.fetch(:height, '37px') %>" width="<%= local_assigns.fetch(:width, '37px') %>" class="<%= local_assigns[:class] %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.998 511.998" xml:space="preserve">
<path style="fill:#C8AF9B;" d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224
S503.999,119.869,503.999,247.999z" />
<path style="fill:#AA968C;" d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -1,18 +1,32 @@
<% attachment_field_uuids = @submitter.submission.template.fields.select { |f| f['type'].in?(%w[image signature attachment]) }.pluck('uuid') %>
<% attachment_field_uuids = @submitter.submission.template.fields.select { |f| f['type'].in?(%w[image signature file]) }.pluck('uuid') %>
<% attachments = ActiveStorage::Attachment.where(uuid: @submitter.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<div class="mx-auto block" style="max-width: 1000px">
<div class="mx-auto block pb-72" style="max-width: 1000px">
<div class="mt-4 flex">
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span>DocuSeal</span>
</a>
</div>
<% @submitter.submission.template.schema.each do |item| %>
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative">
<div class="relative my-4 shadow-md">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute"></div>
</div>
<% end %>
<% end %>
<div class="sticky bottom-8 w-full">
<div class="bg-white mx-8 md:mx-32 border p-4 rounded">
<div class="text-center">
Powered by
<a href="https://www.docuseal.co" class="underline">DocuSeal</a> - An open source documents software
</div>
</div>
<div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32">
<div class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border border-base-200 p-4 rounded">
<submission-form data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-attachments="<%= attachments.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div>
</div>
</div>
</div>

Loading…
Cancel
Save