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
def create
submitter = Template.find_by(slug: params[:template].to_unsafe_h[:slug]) || Submitter.find_by!(slug: params[:submitter_slug])
attachment = Submitters.create_attachment!(submitter, params)
record = if params[:template_slug].present?
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])
end

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

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

@ -36,6 +36,30 @@
/>
</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
v-else
class="flex absolute lg:text-base"
@ -204,6 +228,7 @@ export default {
IconPaperclip,
IconCheck
},
inject: ['templateAttachments'],
props: {
field: {
type: Object,
@ -279,7 +304,9 @@ export default {
multiple: 'Multiple Select',
phone: 'Phone',
redact: 'Redact',
my_text: 'My Text'
my_text: 'Text',
my_signature: 'My Signature',
my_initials: 'My Initials'
}
},
fieldIcons () {
@ -320,6 +347,27 @@ export default {
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 () {
if (this.field.type === '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)"
/>
</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 -->
</div>
<DateStep
@ -371,7 +371,8 @@ export default {
provide () {
return {
baseUrl: this.baseUrl,
t: this.t
t: this.t,
templateAttachments: this.templateAttachments
}
},
props: {
@ -383,6 +384,11 @@ export default {
type: Object,
required: true
},
templateAttachments: {
type: Array,
required: false,
default: () => []
},
canSendEmail: {
type: Boolean,
required: false,
@ -686,7 +692,7 @@ export default {
stepPromise().then(async () => {
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'
return false
} else {

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

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

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

@ -31,7 +31,7 @@
/>
</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"
>
<template v-if="field.type != 'phone'">
@ -164,7 +164,7 @@
Draw New Area
</a>
</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
href="#"
class="text-sm py-1 px-2"

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

@ -72,7 +72,7 @@
:key="type"
>
<div
v-if="!['redact', 'my_text', 'my_signature'].includes(type)"
v-if="!['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)"
>
<button
v-if="withPhone || type != 'phone'"
@ -131,7 +131,7 @@
:key="type"
>
<div
v-if="['redact', 'my_text', 'my_signature'].includes(type)"
v-if="['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)"
>
<button
draggable="true"
@ -292,7 +292,7 @@ export default {
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['redact', 'my_text'].includes(type)) {
if (['redact', 'my_text', 'my_signature', 'my_initials'].includes(type)) {
field.required = 'false'
}
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">
<label
class="label text-2xl"
>Signature</label>
>{{ field.name || t('signature') }}</label>
<div class="space-x-2 flex">
<span
v-if="isTextSignature"
@ -21,9 +21,6 @@
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
<span class="hidden sm:inline">
<!-- {{ t('draw') }} -->
</span>
</a>
</span>
<span
@ -38,9 +35,6 @@
@click.prevent="toggleTextInput"
>
<IconTextSize :width="16" />
<span class="hidden sm:inline">
<!-- {{ t }} -->
</span>
</a>
</span>
<span
@ -57,9 +51,6 @@
accept="image/*"
@change="drawImage"
>
<span class="hidden sm:inline">
<!-- {{ t }} -->
</span>
</label>
</span>
<a
@ -79,17 +70,13 @@
>
<IconReload :width="16" />
</a>
<a
href="#"
title="Remove"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('remove')"
<div
class="tooltip btn btn-outline btn-sm font-medium"
:data-tip="'close'"
@click="$emit('hide')"
>
<IconTrashX
:width="20"
:height="20"
/>
</a>
<IconTrashX :width="16" />
</div>
</div>
</div>
<input
@ -150,7 +137,7 @@ export default {
isDirectUpload: {
type: Boolean,
required: true,
default: true
default: false
},
attachmentsIndex: {
type: Object,
@ -168,11 +155,15 @@ export default {
default: ''
},
template: {
type: String,
type: Object,
required: true
},
mySignatureStyle: {
type: Object,
required: true
}
},
emits: ['attached', 'update:model-value', 'start', 'remove'],
emits: ['attached', 'update:model-value', 'start', 'hide'],
data () {
return {
isSignatureStarted: !!this.previousValue,
@ -330,7 +321,7 @@ export default {
return new Promise((resolve) => {
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) {
const { DirectUpload } = await import('@rails/activestorage')
@ -342,7 +333,7 @@ export default {
fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
template: this.template,
template_slug: this.template.slug,
blob_signed_id: data.signed_id,
name: 'attachments'
}),
@ -354,6 +345,22 @@ export default {
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"
@stop-drag="isMove = false"
@remove="$emit('remove-area', item.area)"
@update:my-text="$emit('update:myText', $event)"
@update:my-field="$emit('update:myField', $event)"
/>
<FieldArea
v-if="newArea"
:is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }"
:area="newArea"
@update:my-text="$emit('update:myText', $event)"
@update:my-field="$emit('update:myField', $event)"
/>
</div>
<div
@ -95,7 +95,7 @@ export default {
required: true
}
},
emits: ['draw', 'drop-field', 'remove-area', 'update:myText'],
emits: ['draw', 'drop-field', 'remove-area', 'update:myField'],
data () {
return {
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 %>%">
<% 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">
<% elsif field['type'] == 'file' %>
<div class="px-0.5 flex flex-col justify-center">

@ -59,7 +59,7 @@
<%= render 'submissions/annotation', annot: %>
<% end %>
<% 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']] %>
<% else %>
<% value = values[field['uuid']] %>
@ -144,7 +144,7 @@
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %>
</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">
<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>

@ -2,4 +2,6 @@
<% 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 || {} %>
<% 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)
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
end
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'))
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(
io,
at: [

Loading…
Cancel
Save