creating a complete new my_signature and my_initials fields for both template editer and form submitter

pull/150/merge^2
iozeey 2 years ago
parent b2c051dc4b
commit e0fe5ac399

@ -6,8 +6,12 @@ module Api
skip_authorization_check skip_authorization_check
def create def create
submitter = Template.find_by(slug: params[:template].to_unsafe_h[:slug]) || Submitter.find_by!(slug: params[:submitter_slug]) record = if params[:template_slug].present?
attachment = Submitters.create_attachment!(submitter, params) Template.find_by(slug: params[:template_slug])
else
Submitter.find_by!(slug: params[:submitter_slug])
end
attachment = Submitters.create_attachment!(record, params)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type]) render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end end

@ -81,6 +81,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template: reactive(JSON.parse(this.dataset.template)),
templateAttachments: reactive(JSON.parse(this.dataset.templateAttachmentsIndex)),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',

@ -11,6 +11,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
this.app = createApp(Form, { this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter), submitter: JSON.parse(this.dataset.submitter),
templateValues: JSON.parse(this.dataset.templateValues), templateValues: JSON.parse(this.dataset.templateValues),
templateAttachments: reactive(JSON.parse(this.dataset.templateAttachmentsIndex)),
authenticityToken: this.dataset.authenticityToken, authenticityToken: this.dataset.authenticityToken,
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true', isDirectUpload: this.dataset.isDirectUpload === 'true',

@ -36,6 +36,30 @@
/> />
</div> </div>
<!-- show mySignature prefill with stored value -->
<div
v-else-if="['my_signature', 'my_initials'].includes(field.type)"
class="flex absolute"
:style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
>
<img
v-if="field.type === 'my_signature' && mySignatureUrl"
class="mx-auto"
:src="mySignatureUrl.url"
>
<img
v-else-if="field.type === 'my_initials' && myInitialsUrl"
class="mx-auto"
:src="myInitialsUrl.url"
>
<img
v-else
class="mx-auto"
:src="'#'"
>
</div>
<div <div
v-else v-else
class="flex absolute lg:text-base" class="flex absolute lg:text-base"
@ -204,6 +228,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['templateAttachments'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -279,7 +304,9 @@ export default {
multiple: 'Multiple Select', multiple: 'Multiple Select',
phone: 'Phone', phone: 'Phone',
redact: 'Redact', redact: 'Redact',
my_text: 'My Text' my_text: 'Text',
my_signature: 'My Signature',
my_initials: 'My Initials'
} }
}, },
fieldIcons () { fieldIcons () {
@ -320,6 +347,27 @@ export default {
return null return null
} }
}, },
myAttachmentsIndex () {
return this.templateAttachments.reduce((acc, a) => {
acc[a.uuid] = a
return acc
}, {})
},
mySignatureUrl () {
if (this.field.type === 'my_signature') {
return this.myAttachmentsIndex[this.templateValues[this.field.uuid]]
} else {
return null
}
},
myInitialsUrl () {
if (this.field.type === 'my_initials') {
return this.myAttachmentsIndex[this.templateValues[this.field.uuid]]
} 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))

@ -66,7 +66,7 @@
@focus="$refs.areas.scrollIntoField(currentField)" @focus="$refs.areas.scrollIntoField(currentField)"
/> />
</div> </div>
<div v-if="['my_text'].includes(currentField.type)"> <div v-if="['my_text', 'my_signature', 'my_initials'].includes(currentField.type)">
<!-- do nothing on this side just chill for now --> <!-- do nothing on this side just chill for now -->
</div> </div>
<DateStep <DateStep
@ -371,7 +371,8 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
t: this.t t: this.t,
templateAttachments: this.templateAttachments
} }
}, },
props: { props: {
@ -383,6 +384,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
templateAttachments: {
type: Array,
required: false,
default: () => []
},
canSendEmail: { canSendEmail: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -686,7 +692,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (['redact', 'my_text'].includes(fields[0].type)) { if (['redact', 'my_text', 'my_signature', 'my_initials'].includes(fields[0].type)) {
fields[0].required = 'false' fields[0].required = 'false'
return false return false
} else { } else {

@ -39,7 +39,7 @@
@pointerdown.stop @pointerdown.stop
> >
<FieldSubmitter <FieldSubmitter
v-if="(field.type !== 'my_text')" v-if="!['my_text', 'my_signature', 'my_initials'].includes(field.type)"
v-model="field.submitter_uuid" v-model="field.submitter_uuid"
class="border-r" class="border-r"
:compact="true" :compact="true"
@ -50,7 +50,7 @@
@click="selectedAreaRef.value = area" @click="selectedAreaRef.value = area"
/> />
<FieldType <FieldType
v-if="!['my_text', 'my_signature'].includes(field.type)" v-if="!['my_text', 'my_signature', 'my_initials'].includes(field.type)"
v-model="field.type" v-model="field.type"
:button-width="27" :button-width="27"
:editable="editable" :editable="editable"
@ -70,7 +70,7 @@
@blur="onNameBlur" @blur="onNameBlur"
>{{ optionIndexText }} {{ field.name || defaultName }}</span> >{{ optionIndexText }} {{ field.name || defaultName }}</span>
<div <div
v-if="isNameFocus && !['checkbox', 'phone', 'redact', 'my_text'].includes(field.type)" v-if="isNameFocus && !['checkbox', 'phone', 'redact', 'my_text', 'my_signature', 'my_initials'].includes(field.type)"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
> >
<input <input
@ -129,20 +129,56 @@
@input="makeMyText" @input="makeMyText"
/> />
</div> </div>
<!-- adding my_signature for prefills -->
<div <div
v-else-if="field.type === 'my_signature'" v-else-if="['my_signature'].includes(field.type)"
class="flex items-center justify-center h-full w-full" class="flex items-center justify-center h-full w-full"
style="background-color: white;" style="background-color: white;"
> >
<img <img
v-if="field.type === 'my_signature' && mySignatureUrl"
:id="field.uuid" :id="field.uuid"
:src="field.url" :src="mySignatureUrl.url"
alt="please sign ..."
class="d-flex justify-center w-full h-full"
style="z-index: 50;"
@click="handleMySignatureClick"
>
<img
v-else
:id="field.uuid"
:src="'#'"
alt="please sign ..." alt="please sign ..."
class="d-flex justify-center w-full h-full" class="d-flex justify-center w-full h-full"
style="z-index: 50;" style="z-index: 50;"
@click="handleMySignatureClick" @click="handleMySignatureClick"
> >
</div> </div>
<!-- adding my_initials for prefills -->
<div
v-else-if="['my_initials'].includes(field.type)"
class="flex items-center justify-center h-full w-full"
style="background-color: white;"
>
<img
v-if="field.type === 'my_initials' && myInitialsUrl"
:id="field.uuid"
:src="myInitialsUrl.url"
alt="please sign ..."
class="d-flex justify-center w-full h-full"
style="z-index: 50;"
@click="handleMyInitialClick"
>
<img
v-else
:id="field.uuid"
:src="'#'"
alt="please sign ..."
class="d-flex justify-center w-full h-full"
style="z-index: 50;"
@click="handleMyInitialClick"
>
</div>
<div <div
v-else v-else
class="flex items-center h-full w-full" class="flex items-center h-full w-full"
@ -173,7 +209,7 @@
</span> </span>
</div> </div>
<div <div
v-if="field.type !== 'my_text'" v-if="!['my_text', 'my_signature', 'my_initials'].includes(field.type)"
ref="touchTarget" ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
/> />
@ -187,24 +223,35 @@
<div <div
v-if="showMySignature" v-if="showMySignature"
> >
<!--
<MySignature <MySignature
:key="field.uuid" :key="field.uuid"
v-model="values[field.uuid]" v-model="setSignatureValue"
:style="mySignatureStyle" :my-signature-style="mySignatureStyle"
:is-direct-upload="isDirectUpload"
:field="field" :field="field"
:previous-value="previousSignatureValue" :previous-value="previousSignatureValue"
:is-direct-upload="isDirectUpload" :template="template"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@attached="attachments.push($event)" @attached="handleMySignatureAttachment"
@remove="showMySignature = false" @hide="showMySignature = false"
@start="$refs.areas.scrollIntoField(field)"
/> />
--> </div>
<MySignature <div
v-if="showMyInitials"
>
<MyInitials
:key="field.uuid" :key="field.uuid"
:style="mySignatureStyle" v-model="setInitialsValue"
:my-signature-style="mySignatureStyle"
:is-direct-upload="isDirectUpload"
:field="field" :field="field"
:previous-value="previousInitialsValue"
:template="template" :template="template"
:attachments-index="attachmentsIndex"
@attached="handleMyInitialsAttachment"
@hide="showMySignature = false"
@start="$refs.areas.scrollIntoField(field)"
/> />
</div> </div>
</template> </template>
@ -216,6 +263,7 @@ import Field from './field'
import { IconX, IconWriting } from '@tabler/icons-vue' import { IconX, IconWriting } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import MySignature from './my_signature' import MySignature from './my_signature'
import MyInitials from './my_initials'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@ -224,9 +272,10 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX, IconX,
IconWriting, IconWriting,
MySignature MySignature,
MyInitials
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 'templateAttachments', 'isDirectUpload'],
props: { props: {
area: { area: {
type: Object, type: Object,
@ -237,16 +286,6 @@ export default {
required: false, required: false,
default: false default: false
}, },
// modelValue: {
// type: [Array, String, Number, Object, Boolean],
// required: false,
// default: ''
// },
// attachmentsIndex: {
// type: Object,
// required: false,
// default: () => ({})
// },
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -258,7 +297,7 @@ export default {
default: null default: null
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'update:myText'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'update:myField'],
data () { data () {
return { return {
isResize: false, isResize: false,
@ -267,13 +306,32 @@ export default {
myLocalText: '', myLocalText: '',
textOverflowChars: 0, textOverflowChars: 0,
dragFrom: { x: 0, y: 0 }, dragFrom: { x: 0, y: 0 },
showMySignature: false showMySignature: false,
showMyInitials: false,
myLocalSignatureValue: '',
myLocalInitialsValue: ''
} }
}, },
computed: { computed: {
defaultName: Field.computed.defaultName, defaultName: Field.computed.defaultName,
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
setSignatureValue: {
get () {
return this.myLocalSignatureValue
},
set (value) {
this.makeMySignature(value)
}
},
setInitialsValue: {
get () {
return this.myLocalInitialsValue
},
set (value) {
this.makeMyInitials(value)
}
},
optionIndexText () { optionIndexText () {
if (this.area.option_uuid && this.field.options) { if (this.area.option_uuid && this.field.options) {
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.` return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
@ -281,23 +339,47 @@ export default {
return '' return ''
} }
}, },
attachmentsIndex () {
return this.templateAttachments.reduce((acc, a) => {
acc[a.uuid] = a
return acc
}, {})
},
previousSignatureValue () {
const mySignatureField = (this.field.type === 'my_signature' && !!this.template.values[this.field.uuid])
return this.template.values[mySignatureField?.uuid]
},
previousInitialsValue () {
const initialsField = (this.field.type === 'my_initials' && !!this.template.values[this.field.uuid])
return this.template.values[initialsField?.uuid]
},
mySignatureStyle () { mySignatureStyle () {
const { x, y, w, h } = this.area const { x, y, w, h } = this.area
return { return {
top: (y * 100) + 7 + '%', top: (y * 100) + 7 + '%',
left: (x * 100) - 10 + '%', left: (x * 100) + '%',
width: w * 100 + '%', width: w * 100 + '%',
height: h * 100 + '%' height: h * 100 + '%'
} }
}, },
// my_signature () { mySignatureUrl () {
// if (this.field.type === 'my_signature') { if (this.field.type === 'my_signature') {
// return this.attachmentsIndex[this.modelValue] return this.attachmentsIndex[this.myLocalSignatureValue]
// } else { } else {
// return null return null
// } }
// }, },
myInitialsUrl () {
if (this.field.type === 'my_initials') {
return this.attachmentsIndex[this.myLocalInitialsValue]
} else {
return null
}
},
cells () { cells () {
const cells = [] const cells = []
@ -367,6 +449,26 @@ export default {
} }
}, },
mounted () { mounted () {
if (this.field.type === 'my_signature') {
const fieldUuid = this.field.uuid
if (this.template.values && this.template.values[fieldUuid]) {
this.myLocalSignatureValue = this.template.values[fieldUuid]
} else {
this.myLocalSignatureValue = ''
}
this.saveFieldValue({ [this.field.uuid]: this.myLocalSignatureValue })
}
if (this.field.type === 'my_initials') {
const fieldUuid = this.field.uuid
if (this.template.values && this.template.values[fieldUuid]) {
this.myLocalInitialsValue = this.template.values[fieldUuid]
} else {
this.myLocalInitialsValue = ''
}
this.saveFieldValue({ [this.field.uuid]: this.myLocalInitialsValue })
}
if (this.field.type === 'my_text') { if (this.field.type === 'my_text') {
const fieldUuid = this.field.uuid const fieldUuid = this.field.uuid
if (this.template.values && this.template.values[fieldUuid]) { if (this.template.values && this.template.values[fieldUuid]) {
@ -385,17 +487,38 @@ export default {
methods: { methods: {
makeMyText (e) { makeMyText (e) {
this.myLocalText = e.target.value ? e.target.value : this.myLocalText this.myLocalText = e.target.value ? e.target.value : this.myLocalText
this.sendSaveText( this.saveFieldValue(
{ [this.field.uuid]: e.target.value } { [this.field.uuid]: e.target.value }
) )
}, },
makeMySignature (e) { makeMySignature (value) {
this.sendSaveText( if (value !== null) {
{ [this.field.uuid]: e.attachment.uuid } this.myLocalSignatureValue = value
) this.saveFieldValue({ [this.field.uuid]: value })
} else {
console.log('My signature field value was empty')
this.saveFieldValue({ [this.field.uuid]: '' })
}
},
makeMyInitials (value) {
if (value !== null) {
this.myLocalInitialsValue = value
this.saveFieldValue({ [this.field.uuid]: value })
} else {
console.log('My initial field value was empty')
this.saveFieldValue({ [this.field.uuid]: '' })
}
},
saveFieldValue (event) {
this.$emit('update:myField', event)
}, },
sendSaveText (event) { handleMyInitialsAttachment (attachment) {
this.$emit('update:myText', event) this.templateAttachments.push(attachment)
this.makeMyInitials(attachment.uuid)
},
handleMySignatureAttachment (attachment) {
this.templateAttachments.push(attachment)
this.makeMySignature(attachment.uuid)
}, },
onNameFocus (e) { onNameFocus (e) {
this.selectedAreaRef.value = this.area this.selectedAreaRef.value = this.area
@ -596,8 +719,10 @@ export default {
this.save() this.save()
}, },
handleMySignatureClick () { handleMySignatureClick () {
console.log('my-signature event triggered with field:')
this.showMySignature = !this.showMySignature this.showMySignature = !this.showMySignature
},
handleMyInitialClick () {
this.showMyInitials = !this.showMyInitials
} }
} }
} }

@ -134,7 +134,7 @@
@draw="onDraw" @draw="onDraw"
@drop-field="onDropfield" @drop-field="onDropfield"
@remove-area="removeArea" @remove-area="removeArea"
@update:my-text="updateMyText" @update:my-field="updateMyValues"
/> />
<DocumentControls <DocumentControls
v-if="isBreakpointLg && editable" v-if="isBreakpointLg && editable"
@ -281,6 +281,8 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
templateAttachments: this.templateAttachments,
isDirectUpload: this.isDirectUpload,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
@ -294,6 +296,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
templateAttachments: {
type: Array,
required: false,
default: () => []
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -431,10 +438,11 @@ export default {
}, },
methods: { methods: {
t, t,
updateMyText (values) { updateMyValues (values) {
const existingValues = this.template.values || {} const existingValues = this.template.values || {}
const updatedValues = { ...existingValues, ...values } const updatedValues = { ...existingValues, ...values }
this.template.values = updatedValues this.template.values = updatedValues
this.save()
}, },
startFieldDraw (type) { startFieldDraw (type) {
const field = { const field = {
@ -445,7 +453,7 @@ export default {
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: this.selectedSubmitter.uuid,
type type
} }
if (['redact', 'my_text'].includes(type)) { if (['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)) {
field.required = 'false' field.required = 'false'
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
@ -623,7 +631,7 @@ export default {
...this.dragField ...this.dragField
} }
if (['redact', 'my_text'].includes(field.type)) { if (['redact', 'my_text', 'my_signature', 'my_initials'].includes(field.type)) {
field.required = 'false' field.required = 'false'
} }
if (['select', 'multiple', 'radio'].includes(field.type)) { if (['select', 'multiple', 'radio'].includes(field.type)) {
@ -656,12 +664,12 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
} }
} else if (field.type === 'signature') { } else if (['signature', 'my_signature'].includes(field.type)) {
baseArea = { baseArea = {
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 (field.type === 'initials') { } else if (['initials', 'my_initials'].includes(field.type)) {
baseArea = { baseArea = {
w: area.maskW / 10 / area.maskW, w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW h: area.maskW / 35 / area.maskW

@ -14,7 +14,7 @@
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)" @remove-area="$emit('remove-area', $event)"
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })" @draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
@update:my-text="$emit('update:myText', $event)" @update:my-field="$emit('update:myField', $event)"
/> />
</div> </div>
</template> </template>
@ -61,7 +61,7 @@ export default {
default: false default: false
} }
}, },
emits: ['draw', 'drop-field', 'remove-area', 'update:myText'], emits: ['draw', 'drop-field', 'remove-area', 'update:myField'],
data () { data () {
return { return {
pageRefs: [] pageRefs: []

@ -31,7 +31,7 @@
/> />
</div> </div>
<div <div
v-if="isNameFocus && !['redact', 'my_text'].includes(field.type)" v-if="isNameFocus && !['redact', 'my_text', 'my_signature', 'my_initials'].includes(field.type)"
class="flex items-center relative" class="flex items-center relative"
> >
<template v-if="field.type != 'phone'"> <template v-if="field.type != 'phone'">
@ -164,7 +164,7 @@
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)"> <li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'my_text', 'redact', 'my_signature', 'my_initials'].includes(field.type)">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"

@ -93,8 +93,9 @@ export default {
cells: 'Cells', cells: 'Cells',
phone: 'Phone', phone: 'Phone',
redact: 'Redact', redact: 'Redact',
my_text: 'My_Text', my_text: 'Text',
my_signature: 'My Signature' my_signature: 'My Signature',
my_initials: 'My Initials'
} }
}, },
fieldIcons () { fieldIcons () {
@ -113,7 +114,8 @@ export default {
phone: IconPhoneCheck, phone: IconPhoneCheck,
redact: IconBarrierBlock, redact: IconBarrierBlock,
my_text: IconTextResize, my_text: IconTextResize,
my_signature: IconWritingSign my_signature: IconWritingSign,
my_initials: IconLetterCaseUpper
} }
} }
}, },

@ -72,7 +72,7 @@
:key="type" :key="type"
> >
<div <div
v-if="!['redact', 'my_text', 'my_signature'].includes(type)" v-if="!['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)"
> >
<button <button
v-if="withPhone || type != 'phone'" v-if="withPhone || type != 'phone'"
@ -131,7 +131,7 @@
:key="type" :key="type"
> >
<div <div
v-if="['redact', 'my_text', 'my_signature'].includes(type)" v-if="['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)"
> >
<button <button
draggable="true" draggable="true"
@ -292,7 +292,7 @@ export default {
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: this.selectedSubmitter.uuid,
type type
} }
if (['redact', 'my_text'].includes(type)) { if (['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)) {
field.required = 'false' field.required = 'false'
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {

@ -0,0 +1,292 @@
<template>
<div
class="absolute"
style="z-index: 50;"
:style="{ ...mySignatureStyle }"
>
<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
v-if="isDrawInitials"
class="tooltip"
:data-tip="t('type_initials')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
@click.prevent="toggleTextInput"
>
<IconTextSize :width="16" />
</a>
</span>
<span
v-else
class="tooltip"
:data-tip="t('draw_initials')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
</a>
</span>
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn font-medium btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
</a>
<a
v-else
href="#"
class="btn font-medium btn-outline btn-sm"
@click.prevent="clear"
>
<IconReload :width="16" />
</a>
<div
class="tooltip btn btn-outline btn-sm font-medium"
:data-tip="'close'"
@click="$emit('hide')"
>
<IconTrashX :width="16" />
</div>
</div>
</div>
<input
:value="modelValue || computedPreviousValue"
type="hidden"
:name="`values[${field.uuid}]`"
>
<img
v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72"
>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl"
/>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
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"
>
<button
class="btn btn-outline w-full mt-2"
@click="submit"
>
<span> Submit </span>
</button>
</div>
</template>
<script>
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconTrashX } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad'
export default {
name: 'MyInitials',
components: {
IconReload,
IconTextSize,
IconSignature,
IconTrashX
},
inject: ['baseUrl', 't'],
props: {
field: {
type: Object,
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
attachmentsIndex: {
type: Object,
required: false,
default: () => ({})
},
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: {
type: String,
required: false,
default: ''
},
template: {
type: Object,
required: true
},
mySignatureStyle: {
type: Object,
required: true
}
},
emits: ['attached', 'update:model-value', 'start', 'hide', 'focus'],
data () {
return {
isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true,
isDrawInitials: false
}
},
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
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')
}
if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true
this.$emit('start')
})
}
},
methods: {
remove () {
this.$emit('update:model-value', '')
this.isUsePreviousValue = false
this.isInitialsStarted = false
},
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 || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({})
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'my_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({
template_slug: this.template.slug,
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('template_slug', this.template.slug)
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>

@ -7,7 +7,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"
>Signature</label> >{{ field.name || t('signature') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isTextSignature" v-if="isTextSignature"
@ -21,9 +21,6 @@
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
<span class="hidden sm:inline">
<!-- {{ t('draw') }} -->
</span>
</a> </a>
</span> </span>
<span <span
@ -38,9 +35,6 @@
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
<span class="hidden sm:inline">
<!-- {{ t }} -->
</span>
</a> </a>
</span> </span>
<span <span
@ -57,9 +51,6 @@
accept="image/*" accept="image/*"
@change="drawImage" @change="drawImage"
> >
<span class="hidden sm:inline">
<!-- {{ t }} -->
</span>
</label> </label>
</span> </span>
<a <a
@ -79,17 +70,13 @@
> >
<IconReload :width="16" /> <IconReload :width="16" />
</a> </a>
<a <div
href="#" class="tooltip btn btn-outline btn-sm font-medium"
title="Remove" :data-tip="'close'"
class="py-1.5 inline md:hidden" @click="$emit('hide')"
@click.prevent="$emit('remove')"
> >
<IconTrashX <IconTrashX :width="16" />
:width="20" </div>
:height="20"
/>
</a>
</div> </div>
</div> </div>
<input <input
@ -150,7 +137,7 @@ export default {
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
default: true default: false
}, },
attachmentsIndex: { attachmentsIndex: {
type: Object, type: Object,
@ -168,11 +155,15 @@ export default {
default: '' default: ''
}, },
template: { template: {
type: String, type: Object,
required: true
},
mySignatureStyle: {
type: Object,
required: true required: true
} }
}, },
emits: ['attached', 'update:model-value', 'start', 'remove'], emits: ['attached', 'update:model-value', 'start', 'hide'],
data () { data () {
return { return {
isSignatureStarted: !!this.previousValue, isSignatureStarted: !!this.previousValue,
@ -330,7 +321,7 @@ export default {
return new Promise((resolve) => { return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' }) const file = new File([blob], 'my_signature.png', { type: 'image/png' })
if (this.isDirectUpload) { if (this.isDirectUpload) {
const { DirectUpload } = await import('@rails/activestorage') const { DirectUpload } = await import('@rails/activestorage')
@ -342,7 +333,7 @@ export default {
fetch(this.baseUrl + '/api/attachments', { fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
template: this.template, template_slug: this.template.slug,
blob_signed_id: data.signed_id, blob_signed_id: data.signed_id,
name: 'attachments' name: 'attachments'
}), }),
@ -354,6 +345,22 @@ export default {
return resolve(attachment) return resolve(attachment)
}) })
}) })
} else {
const formData = new FormData()
formData.append('file', file)
formData.append('template_slug', this.template.slug)
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)
})
} }
}) })
}) })

@ -28,14 +28,14 @@
@start-drag="isMove = true" @start-drag="isMove = true"
@stop-drag="isMove = false" @stop-drag="isMove = false"
@remove="$emit('remove-area', item.area)" @remove="$emit('remove-area', item.area)"
@update:my-text="$emit('update:myText', $event)" @update:my-field="$emit('update:myField', $event)"
/> />
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
:is-draw="true" :is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }"
:area="newArea" :area="newArea"
@update:my-text="$emit('update:myText', $event)" @update:my-field="$emit('update:myField', $event)"
/> />
</div> </div>
<div <div
@ -95,7 +95,7 @@ export default {
required: true required: true
} }
}, },
emits: ['draw', 'drop-field', 'remove-area', 'update:myText'], emits: ['draw', 'drop-field', 'remove-area', 'update:myField'],
data () { data () {
return { return {
areaRefs: [], areaRefs: [],

@ -1,5 +1,5 @@
<div class="flex absolute <%= field['readonly'] ? 'text-[1.5vw] lg:text-xs' : '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 <%= field['readonly'] ? 'text-[1.5vw] lg:text-xs' : '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', 'initials']) %> <% if field['type'].in?(['signature', 'image', 'initials', 'my_signature', 'my_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">

@ -59,7 +59,7 @@
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% if ['my_text'].include?(field['type']) %> <% if ['my_text', 'my_signature', 'my_initials'].include?(field['type']) %>
<% value = @submission.template.values[field['uuid']] %> <% value = @submission.template.values[field['uuid']] %>
<% else %> <% else %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
@ -144,7 +144,7 @@
<%= 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'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials my_signature my_initials]) %>
<div class="w-full bg-base-300 py-1"> <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>

@ -2,4 +2,6 @@
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% completed_button_params = submitter.submission.template.account.account_configs.find_by(key: AccountConfig::FORM_COMPLETED_BUTTON_KEY)&.value || {} %> <% completed_button_params = submitter.submission.template.account.account_configs.find_by(key: AccountConfig::FORM_COMPLETED_BUTTON_KEY)&.value || {} %>
<% templateValues = submitter.submission.template.values %> <% templateValues = submitter.submission.template.values %>
<submission-form data-template-values="<%= templateValues.to_json %>" data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= completed_button_params.to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form> <% template_attachments = ActiveStorage::Attachment.where(record: submitter.submission.template, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% template_attachments_index = template_attachments.values.select { |e| e.record_id == submitter.submission.template.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<submission-form data-template-attachments-index="<%= template_attachments_index %>" data-template-values="<%= templateValues.to_json %>" data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= completed_button_params.to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>

@ -1 +1,3 @@
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder> <% attachments_index = ActiveStorage::Attachment.where(record: @template, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% template_attachments_index = attachments_index.values.select { |e| e.record_id == @template.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>" data-template-attachments-index="<%= template_attachments_index %>"></template-builder>

@ -33,7 +33,7 @@ module Submissions
pdfs_index = build_pdfs_index(submitter) pdfs_index = build_pdfs_index(submitter)
submitter.submission.template_fields.each do |field| submitter.submission.template_fields.each do |field|
unless ['my_text'].include?(field['type']) unless ['my_text', 'my_signature', 'my_initials'].include?(field['type'])
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
end end
field.fetch('areas', []).each do |area| field.fetch('areas', []).each do |area|
@ -71,6 +71,25 @@ module Submissions
io = StringIO.new(image.resize([scale * 4, 1].min).write_to_buffer('.png')) io = StringIO.new(image.resize([scale * 4, 1].min).write_to_buffer('.png'))
canvas.image(
io,
at: [
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
height - (area['y'] * height) - (image.height * scale / 2) - (area['h'] * height / 2)
],
width: image.width * scale,
height: image.height * scale
)
when 'my_signature', 'my_initials'
attachment = ActiveStorage::Attachment.where(record: template, name: :attachments).preload(:blob).index_by(&:uuid).values.find{ |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
scale = [(area['w'] * width) / image.width,
(area['h'] * height) / image.height].min
io = StringIO.new(image.resize([scale * 4, 1].min).write_to_buffer('.png'))
canvas.image( canvas.image(
io, io,
at: [ at: [

Loading…
Cancel
Save