add initials field

pull/133/head
Alex Turchyn 2 years ago
parent dc02964d9b
commit ee350324e5

@ -74,3 +74,20 @@ button[disabled] .enabled {
.base-select { .base-select {
@apply select base-input w-full font-normal; @apply select base-input w-full font-normal;
} }
.tooltip-bottom-end:before {
transform: translateX(-95%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-end:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}

@ -47,6 +47,11 @@
class="object-contain mx-auto" class="object-contain mx-auto"
:src="signature.url" :src="signature.url"
> >
<img
v-else-if="field.type === 'initials' && initials"
class="object-contain mx-auto"
:src="initials.url"
>
<div <div
v-else-if="field.type === 'file'" v-else-if="field.type === 'file'"
class="px-0.5 flex flex-col justify-center" class="px-0.5 flex flex-col justify-center"
@ -113,7 +118,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@ -175,6 +180,7 @@ export default {
signature: 'Signature', signature: 'Signature',
date: 'Date', date: 'Date',
image: 'Image', image: 'Image',
initials: 'Initials',
file: 'File', file: 'File',
select: 'Select', select: 'Select',
checkbox: 'Checkbox', checkbox: 'Checkbox',
@ -189,6 +195,7 @@ export default {
signature: IconWritingSign, signature: IconWritingSign,
date: IconCalendarEvent, date: IconCalendarEvent,
image: IconPhoto, image: IconPhoto,
initials: IconLetterCaseUpper,
file: IconPaperclip, file: IconPaperclip,
select: IconSelect, select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
@ -212,6 +219,13 @@ export default {
return null return null
} }
}, },
initials () {
if (this.field.type === 'initials') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
formattedDate () { formattedDate () {
if (this.field.type === 'date' && this.modelValue) { if (this.field.type === 'date' && this.modelValue) {
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue)) return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))

@ -256,6 +256,19 @@
@start="$refs.areas.scrollIntoField(currentField)" @start="$refs.areas.scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep
v-else-if="currentField.type === 'initials'"
ref="currentStep"
v-model="values[currentField.uuid]"
:field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)"
@focus="$refs.areas.scrollIntoField(currentField)"
@minimize="isFormVisible = false"
/>
<AttachmentStep <AttachmentStep
v-else-if="currentField.type === 'file'" v-else-if="currentField.type === 'file'"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
@ -328,6 +341,7 @@
import FieldAreas from './areas' import FieldAreas from './areas'
import ImageStep from './image_step' import ImageStep from './image_step'
import SignatureStep from './signature_step' import SignatureStep from './signature_step'
import InitialsStep from './initials_step'
import AttachmentStep from './attachment_step' import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
@ -342,6 +356,7 @@ export default {
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AttachmentStep, AttachmentStep,
InitialsStep,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
IconArrowsDiagonal, IconArrowsDiagonal,
@ -439,7 +454,8 @@ export default {
if (this.recalculateButtonDisabledKey) { if (this.recalculateButtonDisabledKey) {
return this.isSubmitting || return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) || (this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) (this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
} else { } else {
return false return false
} }
@ -566,7 +582,7 @@ export default {
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
const stepPromise = ['signature', 'phone'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})

@ -10,6 +10,13 @@ const en = {
or_drag_and_drop_files: 'or drag and drop files', or_drag_and_drop_files: 'or drag and drop files',
send_copy_via_email: 'Send copy via email', send_copy_via_email: 'Send copy via email',
download: 'Download', download: 'Download',
signature: 'Signature',
initials: 'Initials',
clear: 'Clear',
redraw: 'Redraw',
draw_initials: 'Draw initials',
type_signature_here: 'Type signature here',
type_initial_here: 'Type initials here',
form_has_been_completed: 'Form has been completed!', form_has_been_completed: 'Form has been completed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
signed_with: 'Signed with', signed_with: 'Signed with',
@ -36,6 +43,13 @@ const es = {
or_drag_and_drop_files: 'o arrastra y suelta archivos', or_drag_and_drop_files: 'o arrastra y suelta archivos',
send_copy_via_email: 'Enviar copia por correo electrónico', send_copy_via_email: 'Enviar copia por correo electrónico',
download: 'Descargar', download: 'Descargar',
signature: 'Firma',
initials: 'Iniciales',
clear: 'Borrar',
redraw: 'Redibujar',
draw_initials: 'Dibujar iniciales',
type_signature_here: 'Escribe la firma aquí',
type_initial_here: 'Escribe las iniciales aquí',
form_has_been_completed: '¡El formulario ha sido completado!', form_has_been_completed: '¡El formulario ha sido completado!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
signed_with: 'Firmado con', signed_with: 'Firmado con',
@ -62,6 +76,13 @@ const it = {
or_drag_and_drop_files: 'oppure trascina e rilascia i file', or_drag_and_drop_files: 'oppure trascina e rilascia i file',
send_copy_via_email: 'Invia copia via email', send_copy_via_email: 'Invia copia via email',
download: 'Scarica', download: 'Scarica',
signature: 'Firma',
initials: 'Iniziali',
clear: 'Cancella',
redraw: 'Ridisegna',
draw_initials: 'Disegna iniziali',
type_signature_here: 'Scrivi la firma qui',
type_initial_here: 'Scrivi le iniziali qui',
form_has_been_completed: 'Il modulo è stato completato!', form_has_been_completed: 'Il modulo è stato completato!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
signed_with: 'Firmato con', signed_with: 'Firmato con',
@ -88,6 +109,13 @@ const de = {
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen', or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
send_copy_via_email: 'Kopie per E-Mail senden', send_copy_via_email: 'Kopie per E-Mail senden',
download: 'Herunterladen', download: 'Herunterladen',
signature: 'Unterschrift',
initials: 'Initialen',
clear: 'Löschen',
redraw: 'Neu zeichnen',
draw_initials: 'Initialen zeichnen',
type_signature_here: 'Unterschrift hier eingeben',
type_initial_here: 'Initialen hier eingeben',
form_has_been_completed: 'Formular wurde ausgefüllt!', form_has_been_completed: 'Formular wurde ausgefüllt!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
signed_with: 'Unterschrieben mit', signed_with: 'Unterschrieben mit',
@ -114,6 +142,13 @@ const fr = {
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers', or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
send_copy_via_email: 'Envoyer une copie par e-mail', send_copy_via_email: 'Envoyer une copie par e-mail',
download: 'Télécharger', download: 'Télécharger',
signature: 'Signature',
initials: 'Initiales',
clear: 'Effacer',
redraw: 'Redessiner',
draw_initials: 'Dessiner les initiales',
type_signature_here: 'Tapez la signature ici',
type_initial_here: 'Tapez les initiales ici',
form_has_been_completed: 'Le formulaire a été complété !', form_has_been_completed: 'Le formulaire a été complété !',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
signed_with: 'Signé avec', signed_with: 'Signé avec',
@ -140,6 +175,13 @@ const pl = {
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki', or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
send_copy_via_email: 'Wyślij kopię drogą mailową', send_copy_via_email: 'Wyślij kopię drogą mailową',
download: 'Pobierz', download: 'Pobierz',
signature: 'Podpis',
initials: 'Inicjały',
clear: 'Wyczyść',
redraw: 'Przerysuj',
draw_initials: 'Narysuj inicjały',
type_signature_here: 'Wpisz podpis tutaj',
type_initial_here: 'Wpisz inicjały tutaj',
form_has_been_completed: 'Formularz został wypełniony!', form_has_been_completed: 'Formularz został wypełniony!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
signed_with: 'Podpisane za pomocą', signed_with: 'Podpisane za pomocą',
@ -166,6 +208,13 @@ const uk = {
or_drag_and_drop_files: 'або перетягніть файли сюди', or_drag_and_drop_files: 'або перетягніть файли сюди',
send_copy_via_email: 'Надіслати копію електронною поштою', send_copy_via_email: 'Надіслати копію електронною поштою',
download: 'Завантажити', download: 'Завантажити',
signature: 'Підпис',
initials: 'Ініціали',
clear: 'Очистити',
redraw: 'Перемалювати',
draw_initials: 'Намалювати ініціали',
type_signature_here: 'Введіть підпис тут',
type_initial_here: 'Введіть ініціали тут',
form_has_been_completed: 'Форму заповнено!', form_has_been_completed: 'Форму заповнено!',
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
signed_with: 'Підписано за допомогою', signed_with: 'Підписано за допомогою',
@ -192,6 +241,13 @@ const cs = {
or_drag_and_drop_files: 'nebo přetáhněte soubory sem', or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
send_copy_via_email: 'Odeslat kopii e-mailem', send_copy_via_email: 'Odeslat kopii e-mailem',
download: 'Stáhnout', download: 'Stáhnout',
signature: 'Podpis',
initials: 'Iniciály',
clear: 'Smazat',
redraw: 'Překreslit',
draw_initials: 'Nakreslit iniciály',
type_signature_here: 'Sem zadejte podpis',
type_initial_here: 'Sem zadejte iniciály',
form_has_been_completed: 'Formulář byl dokončen!', form_has_been_completed: 'Formulář byl dokončen!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
signed_with: 'Podepsáno pomocí', signed_with: 'Podepsáno pomocí',
@ -218,6 +274,13 @@ const pt = {
or_drag_and_drop_files: 'ou arraste e solte arquivos', or_drag_and_drop_files: 'ou arraste e solte arquivos',
send_copy_via_email: 'Enviar cópia por e-mail', send_copy_via_email: 'Enviar cópia por e-mail',
download: 'Baixar', download: 'Baixar',
signature: 'Assinatura',
initials: 'Iniciais',
clear: 'Limpar',
redraw: 'Redesenhar',
draw_initials: 'Desenhar iniciais',
type_signature_here: 'Digite a assinatura aqui',
type_initial_here: 'Digite as iniciais aqui',
form_has_been_completed: 'O formulário foi concluído!', form_has_been_completed: 'O formulário foi concluído!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
signed_with: 'Assinado com', signed_with: 'Assinado com',

@ -0,0 +1,245 @@
<template>
<div>
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || t('initials') }}</label>
<div class="space-x-2 flex">
<span
class="tooltip"
:data-tip="t('draw_initials')"
>
<button
id="type_text_button"
class="btn btn-sm btn-circle"
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
</button>
</span>
<button
v-if="modelValue"
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
{{ t('clear') }}
</button>
<button
v-else
class="btn btn-outline btn-sm"
@click.prevent="clear"
>
<IconReload :width="16" />
{{ t('clear') }}
</button>
<button
title="Minimize"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"
>
<IconArrowsDiagonalMinimize2
:width="20"
:height="20"
/>
</button>
</div>
</div>
<input
:value="modelValue"
type="hidden"
:name="`values[${field.uuid}]`"
>
<img
v-if="modelValue"
:src="attachmentsIndex[modelValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72"
>
<canvas
v-show="!modelValue"
ref="canvas"
class="bg-white border border-base-300 rounded"
/>
<input
v-if="!isDrawInitials"
id="initials_text_input"
ref="textInput"
class="base-input !text-2xl w-full mt-6 text-center"
:required="field.required && !isInitialsStarted"
:placeholder="`${t('type_initial_here')}...`"
type="text"
@focus="$emit('focus')"
@input="updateWrittenInitials"
>
</div>
</template>
<script>
import SignatureStep from './signature_step'
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
export default {
name: 'InitialsStep',
components: {
IconReload,
IconSignature,
IconArrowsDiagonalMinimize2
},
inject: ['baseUrl', 't'],
props: {
field: {
type: Object,
required: true
},
submitterSlug: {
type: String,
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
attachmentsIndex: {
type: Object,
required: false,
default: () => ({})
},
modelValue: {
type: String,
required: false,
default: ''
}
},
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () {
return {
isInitialsStarted: false,
isDrawInitials: false
}
},
async mounted () {
this.$nextTick(() => {
if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5
}
this.$refs.textInput?.focus()
})
if (this.isDirectUpload) {
import('@rails/activestorage')
}
const { default: SignaturePad } = await import('signature_pad')
if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true
this.$emit('start')
})
}
},
methods: {
cropCanvasAndExportToPNG: SignatureStep.methods.cropCanvasAndExportToPNG,
remove () {
this.$emit('update:model-value', '')
},
clear () {
this.pad.clear()
this.isInitialsStarted = false
if (this.$refs.textInput) {
this.$refs.textInput.value = ''
}
},
updateWrittenInitials (e) {
this.isInitialsStarted = true
const canvas = this.$refs.canvas
const context = canvas.getContext('2d')
const fontFamily = 'Arial'
const fontSize = '44px'
const fontStyle = 'italic'
const fontWeight = ''
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
},
toggleTextInput () {
this.remove()
this.clear()
this.isDrawInitials = !this.isDrawInitials
if (!this.isDrawInitials) {
this.$nextTick(() => {
this.$refs.textInput.focus()
this.$emit('start')
})
}
},
async submit () {
if (this.modelValue) {
return Promise.resolve({})
}
return new Promise((resolve) => {
this.cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'initials.png', { type: 'image/png' })
if (this.isDirectUpload) {
const { DirectUpload } = await import('@rails/activestorage')
new DirectUpload(
file,
'/direct_uploads'
).create((_error, data) => {
fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
blob_signed_id: data.signed_id,
name: 'attachments'
}),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
return resolve(attachment)
})
})
} else {
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment)
})
}
})
})
}
}
}
</script>

@ -3,7 +3,7 @@
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || 'Signature' }}</label> >{{ field.name || t('signature') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
class="tooltip" class="tooltip"
@ -40,7 +40,7 @@
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
Redraw {{ t('redraw') }}
</button> </button>
<button <button
v-else v-else
@ -48,7 +48,7 @@
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
Clear {{ t('clear') }}
</button> </button>
<button <button
title="Minimize" title="Minimize"
@ -83,7 +83,7 @@
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6" class="base-input !text-2xl w-full mt-6"
:required="field.required" :required="field.required"
:placeholder="`Type signature here...`" :placeholder="`${t('type_signature_here')}...`"
type="text" type="text"
@input="updateWrittenSignature" @input="updateWrittenSignature"
> >
@ -101,7 +101,7 @@ export default {
IconTextSize, IconTextSize,
IconArrowsDiagonalMinimize2 IconArrowsDiagonalMinimize2
}, },
inject: ['baseUrl'], inject: ['baseUrl', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -136,8 +136,10 @@ export default {
}, },
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth if (this.$refs.canvas) {
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3 this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3
}
}) })
if (this.isDirectUpload) { if (this.isDirectUpload) {
@ -146,13 +148,15 @@ export default {
const { default: SignaturePad } = await import('signature_pad') const { default: SignaturePad } = await import('signature_pad')
this.pad = new SignaturePad(this.$refs.canvas) if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
this.$emit('start') this.$emit('start')
}) })
}
}, },
methods: { methods: {
remove () { remove () {

@ -28,7 +28,7 @@
<a <a
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="btn btn-primary" class="btn btn-primary text-base"
> >
<IconUsersPlus <IconUsersPlus
width="20" width="20"
@ -46,12 +46,12 @@
> >
<IconInnerShadowTop <IconInnerShadowTop
v-if="isSaving" v-if="isSaving"
width="20" width="22"
class="animate-spin" class="animate-spin"
/> />
<IconDeviceFloppy <IconDeviceFloppy
v-else v-else
width="20" width="22"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Save Save
@ -473,6 +473,11 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
} }
} else if (this.dragFieldType === 'initials') {
baseArea = {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
}
} else { } else {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,

@ -99,6 +99,19 @@
Draw New Area Draw New Area
</a> </a>
</li> </li>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="copyToAllPages(field)"
>
<IconCopy
:width="20"
:stroke-width="1.6"
/>
Copy to All Pages
</a>
</li>
</ul> </ul>
</span> </span>
<button <button
@ -183,7 +196,7 @@
<script> <script>
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import FieldType from './field_type' import FieldType from './field_type'
import { IconShape, IconNewSection, IconTrashX } from '@tabler/icons-vue' import { IconShape, IconNewSection, IconTrashX, IconCopy } from '@tabler/icons-vue'
export default { export default {
name: 'TemplateField', name: 'TemplateField',
@ -192,6 +205,7 @@ export default {
IconShape, IconShape,
IconNewSection, IconNewSection,
IconTrashX, IconTrashX,
IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save'], inject: ['template', 'save'],
@ -221,6 +235,23 @@ export default {
} }
}, },
methods: { methods: {
copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0])
this.template.documents.forEach((attachment) => {
attachment.preview_images.forEach((page) => {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) })
}
})
})
this.$nextTick(() => {
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
})
this.save()
},
onNameFocus (e) { onNameFocus (e) {
this.isNameFocus = true this.isNameFocus = true

@ -43,7 +43,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
@ -75,6 +75,7 @@ export default {
return { return {
text: 'Text', text: 'Text',
signature: 'Signature', signature: 'Signature',
initials: 'Initials',
date: 'Date', date: 'Date',
image: 'Image', image: 'Image',
file: 'File', file: 'File',
@ -90,14 +91,15 @@ export default {
return { return {
text: IconTextSize, text: IconTextSize,
signature: IconWritingSign, signature: IconWritingSign,
initials: IconLetterCaseUpper,
date: IconCalendarEvent, date: IconCalendarEvent,
image: IconPhoto, image: IconPhoto,
file: IconPaperclip, file: IconPaperclip,
select: IconSelect, select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
cells: IconColumns3,
multiple: IconChecks,
radio: IconCircleDot, radio: IconCircleDot,
multiple: IconChecks,
cells: IconColumns3,
phone: IconPhoneCheck phone: IconPhoneCheck
} }
} }

@ -73,7 +73,7 @@
</button> </button>
<div <div
v-else v-else
class="tooltip flex" class="tooltip tooltip-bottom-end flex"
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification." data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
> >
<a <a

@ -1,5 +1,5 @@
<div class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <div class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image']) %> <% if field['type'].in?(['signature', 'image', 'initials']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' %> <% elsif field['type'] == 'file' %>
<div class="px-0.5 flex flex-col justify-center"> <div class="px-0.5 flex flex-col justify-center">

@ -136,8 +136,8 @@
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %>
</div> </div>
<div> <div>
<% if field['type'] == 'signature' %> <% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
</div> </div>
<% elsif field['type'] == 'image' %> <% elsif field['type'] == 'image' %>

@ -192,7 +192,7 @@ module Submissions
} }
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] ].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
), ),
if field['type'].in?(%w[image signature]) if field['type'].in?(%w[image signature initials])
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot image = Vips::Image.new_from_buffer(attachment.download, '').autorot
@ -200,7 +200,7 @@ module Submissions
io = StringIO.new(image.resize([scale, 1].min).write_to_buffer('.png')) io = StringIO.new(image.resize([scale, 1].min).write_to_buffer('.png'))
column.image(io, padding: [0, 100, 10, 0]) column.image(io, padding: [0, field['type'] == 'initials' ? 200 : 100, 10, 0])
column.formatted_text_box([{ text: '' }]) column.formatted_text_box([{ text: '' }])
elsif field['type'] == 'file' elsif field['type'] == 'file'
column.formatted_text_box( column.formatted_text_box(

@ -58,7 +58,7 @@ module Submissions
canvas.font(FONT_NAME, size: font_size) canvas.font(FONT_NAME, size: font_size)
case field['type'] case field['type']
when 'image', 'signature' when 'image', 'signature', 'initials'
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot image = Vips::Image.new_from_buffer(attachment.download, '').autorot

Loading…
Cancel
Save