2023-05-25 22:47:52 +03:00
< template >
2024-05-12 12:08:18 +03:00
< div
dir = "auto"
class = "relative"
>
2024-03-09 18:24:21 +02:00
< div
2025-02-02 12:30:01 +02:00
class = "flex justify-between items-end w-full mb-3.5 md:mb-4"
2024-03-09 18:24:21 +02:00
: class = "{ 'mb-2': !field.description }"
>
2023-06-15 01:05:04 +03:00
< label
2025-01-27 13:30:34 +02:00
v-if = "showFieldNames"
2025-03-18 17:34:11 +02:00
class = "label text-xl sm:text-2xl py-0 field-name-label"
2024-03-09 18:24:21 +02:00
>
< MarkdownContent
v-if = "field.title"
:string = "field.title"
/ >
< template v-else >
2025-01-27 13:30:34 +02:00
{{ field.name | | t ( ' signature ' ) }}
2024-03-09 18:24:21 +02:00
< / template >
< / label >
2025-01-27 13:30:34 +02:00
< div class = "space-x-2 flex flex-none" >
2023-07-14 00:23:15 +03:00
< span
2025-07-31 11:25:30 +03:00
v-if = "isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
2025-12-15 10:22:27 +02:00
class = "md:tooltip"
2023-11-18 13:57:49 +02:00
:data-tip = "t('draw_signature')"
2023-07-14 00:23:15 +03:00
>
2026-04-16 12:41:42 +03:00
< button
2023-08-20 12:42:47 +03:00
id = "type_text_button"
2026-06-23 09:09:31 +03:00
ref = "typeTextButton"
2026-04-16 12:41:42 +03:00
type = "button"
:aria-label = "t('draw_signature')"
2025-03-18 17:34:11 +02:00
class = "btn btn-outline btn-sm font-medium type-text-button"
2026-04-16 12:41:42 +03:00
@click ="[toggleTextInput(), hideQr()]"
2023-11-18 13:57:49 +02:00
>
2026-04-16 12:41:42 +03:00
< IconSignature
:width = "16"
aria -hidden = " true "
/ >
2023-11-18 13:57:49 +02:00
< span class = "hidden sm:inline" >
{ { t ( 'draw' ) } }
< / span >
2026-04-16 12:41:42 +03:00
< / button >
2023-11-18 13:57:49 +02:00
< / span >
< span
2025-07-31 11:25:30 +03:00
v-else-if = "withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
2025-12-15 10:22:27 +02:00
class = "md:tooltip ml-2"
2025-01-31 20:11:57 +02:00
: class = "{ 'hidden sm:inline': modelValue || computedPreviousValue }"
2023-11-18 13:57:49 +02:00
:data-tip = "t('type_text')"
>
2026-04-16 12:41:42 +03:00
< button
2023-11-18 13:57:49 +02:00
id = "type_text_button"
2026-06-23 09:09:31 +03:00
ref = "typeTextButton"
2026-04-16 12:41:42 +03:00
type = "button"
:aria-label = "t('type_text')"
2025-03-18 17:34:11 +02:00
class = "btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
2026-04-16 12:41:42 +03:00
@click ="[toggleTextInput(), hideQr()]"
2023-07-14 00:23:15 +03:00
>
2026-04-16 12:41:42 +03:00
< IconTextSize
:width = "16"
aria -hidden = " true "
/ >
2023-11-18 13:57:49 +02:00
< span class = "hidden sm:inline" >
{ { t ( 'type' ) } }
< / span >
2026-04-16 12:41:42 +03:00
< / button >
2023-07-14 00:23:15 +03:00
< / span >
< span
2024-12-19 00:24:16 +02:00
v-if = "format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
2025-12-15 10:22:27 +02:00
class = "md:tooltip"
2025-01-31 20:11:57 +02:00
: class = "{ 'hidden sm:inline': modelValue || computedPreviousValue }"
2024-01-29 19:53:27 +02:00
:data-tip = "t('take_photo')"
2023-07-14 00:23:15 +03:00
>
2026-04-17 14:49:14 +03:00
< button
type = "button"
2026-06-23 09:09:31 +03:00
:aria-label = "t('upload')"
2026-04-16 12:41:42 +03:00
class = "btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
2026-04-17 14:49:14 +03:00
@click ="$refs.takePhotoInput.click()"
2026-04-16 12:41:42 +03:00
>
< IconCamera
:width = "16"
aria -hidden = " true "
/ >
2023-07-14 00:23:15 +03:00
< input
2024-02-18 22:12:28 +02:00
:key = "uploadImageInputKey"
2026-04-17 14:49:14 +03:00
ref = "takePhotoInput"
2023-07-14 00:23:15 +03:00
type = "file"
hidden
accept = "image/*"
@change ="drawImage"
>
2023-11-18 13:57:49 +02:00
< span class = "hidden sm:inline" >
{ { t ( 'upload' ) } }
< / span >
2026-04-17 14:49:14 +03:00
< / button >
2023-07-14 00:23:15 +03:00
< / span >
2026-04-16 12:41:42 +03:00
< button
2023-10-28 01:56:22 +03:00
v-if = "modelValue || computedPreviousValue"
2026-04-16 12:41:42 +03:00
type = "button"
2025-03-18 17:34:11 +02:00
class = "btn btn-outline btn-sm font-medium reupload-button"
2026-04-16 12:41:42 +03:00
@click ="remove"
2023-07-14 00:23:15 +03:00
>
2026-04-16 12:41:42 +03:00
< IconReload
:width = "16"
aria -hidden = " true "
/ >
2024-12-19 00:24:16 +02:00
{ { t ( format === 'upload' ? 'reupload' : 'redraw' ) } }
2026-04-16 12:41:42 +03:00
< / button >
2024-06-06 16:54:16 +03:00
< span
2025-07-31 11:25:30 +03:00
v-if = "withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
2025-12-15 10:22:27 +02:00
class = "md:tooltip before:translate-x-[-90%]"
2025-12-11 10:28:07 +02:00
:data-tip = "t('sign_on_the_touchscreen')"
2023-07-14 00:23:15 +03:00
>
2026-04-16 12:41:42 +03:00
< button
type = "button"
:aria-label = "t('sign_on_the_touchscreen')"
2024-06-06 16:54:16 +03:00
class = "btn btn-sm btn-neutral font-medium hidden md:flex"
: class = "{ 'btn-outline': !isShowQr, 'text-white': isShowQr }"
2026-04-16 12:41:42 +03:00
@click ="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
2024-06-06 16:54:16 +03:00
>
< IconQrcode
:width = "19"
:height = "19"
2026-04-16 12:41:42 +03:00
aria -hidden = " true "
2024-06-06 16:54:16 +03:00
/ >
2026-04-16 12:41:42 +03:00
< / button >
2024-06-06 16:54:16 +03:00
< / span >
2026-04-16 12:41:42 +03:00
< button
type = "button"
2024-02-19 23:52:47 +02:00
:title = "t('minimize')"
2026-04-16 12:41:42 +03:00
:aria-label = "t('minimize')"
2023-07-17 22:48:01 +03:00
class = "py-1.5 inline md:hidden"
2026-04-16 12:41:42 +03:00
@click ="$emit('minimize')"
2023-07-17 22:48:01 +03:00
>
< IconArrowsDiagonalMinimize2
:width = "20"
:height = "20"
2026-04-16 12:41:42 +03:00
aria -hidden = " true "
2023-07-17 22:48:01 +03:00
/ >
2026-04-16 12:41:42 +03:00
< / button >
2023-07-14 00:23:15 +03:00
< / div >
2023-06-15 01:05:04 +03:00
< / div >
2024-03-09 18:24:21 +02:00
< div
v-if = "field.description"
2026-04-16 12:41:42 +03:00
: id = "field.uuid + '-desc'"
2024-03-09 18:24:21 +02:00
dir = "auto"
2025-03-18 17:34:11 +02:00
class = "mb-3 px-1 field-description-text"
2024-03-09 18:24:21 +02:00
>
< MarkdownContent :string = "field.description" / >
< / div >
2024-01-06 17:21:15 +02:00
< AppearsOn :field = "field" / >
2023-05-25 22:47:52 +03:00
< input
2023-10-28 01:56:22 +03:00
: value = "modelValue || computedPreviousValue"
2023-05-25 22:47:52 +03:00
type = "hidden"
:name = "`values[${field.uuid}]`"
>
2026-02-02 12:46:34 +02:00
< input
v-if = "isTouchAttachment"
:value = "touchAttachmentUuid"
type = "hidden"
name = "touch_attachment_uuid"
>
2023-05-25 22:47:52 +03:00
< img
2023-10-28 01:56:22 +03:00
v-if = "modelValue || computedPreviousValue"
: src = "attachmentsIndex[modelValue || computedPreviousValue].url"
2026-04-16 12:41:42 +03:00
: alt = "field.name || t('signature')"
2024-06-11 18:19:21 +03:00
class = "mx-auto bg-white border border-base-300 rounded max-h-44"
2023-05-25 22:47:52 +03:00
>
2024-12-19 00:24:16 +02:00
< FileDropzone
2025-06-09 17:06:20 +03:00
v-if = "format === 'upload' && !modelValue && !computedPreviousValue"
2024-12-19 00:24:16 +02:00
: message = "`${t('upload')} ${field.name || t('signature')}`"
:submitter-slug = "submitterSlug"
:dry-run = "dryRun"
:accept = "'image/*'"
@upload ="[$emit('attached', $event[0]), $emit('update:model-value', $event[0].uuid)]"
/ >
< div
v-else
2025-12-15 10:22:27 +02:00
class = "relative select-none"
2024-12-19 00:24:16 +02:00
>
2024-06-02 14:50:37 +03:00
< div
2024-06-06 16:54:16 +03:00
v-if = "!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
class = "absolute top-0.5 right-0.5"
2024-06-02 14:50:37 +03:00
>
2026-04-16 12:41:42 +03:00
< button
type = "button"
2024-06-06 16:54:16 +03:00
class = "btn btn-ghost font-medium btn-xs md:btn-sm"
2026-04-16 12:41:42 +03:00
@click ="[clear(), hideQr()]"
2024-06-02 14:50:37 +03:00
>
2026-04-16 12:41:42 +03:00
< IconReload
:width = "16"
aria -hidden = " true "
/ >
2024-06-06 16:54:16 +03:00
{ { t ( 'clear' ) } }
2026-04-16 12:41:42 +03:00
< / button >
2024-06-02 14:50:37 +03:00
< / div >
2024-10-31 16:43:43 +02:00
< div
v-if = "isTextSignature"
class = "absolute top-0 right-0 left-0 bottom-0"
/ >
2024-06-02 14:50:37 +03:00
< canvas
v-show = "!modelValue && !computedPreviousValue"
ref = "canvas"
2026-06-23 09:09:31 +03:00
role = "img"
2026-04-16 12:41:42 +03:00
:aria-label = "t('signature_drawing_area')"
2024-06-02 14:50:37 +03:00
style = "padding: 1px; 0"
2025-03-18 17:34:11 +02:00
class = "bg-white border border-base-300 rounded-2xl w-full draw-canvas"
2024-06-02 14:50:37 +03:00
/ >
2024-06-06 16:54:16 +03:00
< div
v-if = "isShowQr"
class = "top-0 bottom-0 right-0 left-0 absolute bg-white rounded-2xl m-0.5"
/ >
2024-06-02 14:50:37 +03:00
< div
2024-07-20 08:54:02 +03:00
v-if = "isShowQr"
2024-06-02 14:50:37 +03:00
class = "top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
>
2026-01-20 17:21:14 +02:00
< div class = "absolute top-1.5 right-1.5" >
2026-04-16 12:41:42 +03:00
< button
type = "button"
2024-06-02 14:50:37 +03:00
class = "btn btn-sm btn-circle btn-normal btn-outline"
2026-04-16 12:41:42 +03:00
:aria-label = "t('close')"
@click ="hideQr"
2024-06-02 14:50:37 +03:00
>
2026-04-16 12:41:42 +03:00
< IconX aria -hidden = " true " / >
< / button >
2024-06-02 14:50:37 +03:00
< / div >
< div class = "flex items-center justify-center w-full h-full p-4" >
< div
class = "bg-white p-4 rounded-xl h-full"
>
< canvas
ref = "qrCanvas"
2026-06-25 15:26:52 +03:00
role = "img"
:aria-label = "t('scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature')"
2024-06-02 14:50:37 +03:00
class = "h-full"
2024-07-20 08:54:02 +03:00
width = "132"
height = "132"
2024-06-02 14:50:37 +03:00
/ >
< / div >
< / div >
< / div >
< / div >
2023-07-14 00:23:15 +03:00
< input
2025-06-30 11:41:07 +03:00
v-if = "isTextSignature && !modelValue && !computedPreviousValue"
2023-08-20 12:42:47 +03:00
id = "signature_text_input"
2023-07-14 00:23:15 +03:00
ref = "textInput"
class = "base-input !text-2xl w-full mt-6"
2024-02-16 18:22:40 +02:00
: required = "field.required && !isSignatureStarted"
2026-06-25 15:26:52 +03:00
: aria -label = " field.name | | t ( ' signature ' ) "
2026-07-22 08:45:20 +03:00
: placeholder = "`${t('type_signature_here')}${field.required ? '...' : ' (' + t('optional') + ')'}`"
2023-07-14 00:23:15 +03:00
type = "text"
@input ="updateWrittenSignature"
>
2024-08-06 00:19:33 +03:00
< select
2025-11-11 13:42:01 +02:00
v-if = "withSigningReason && !isOtherReason"
2024-08-06 00:19:33 +03:00
class = "select base-input !text-2xl w-full mt-6 text-center"
2024-10-10 19:49:31 +03:00
: class = "{ 'text-gray-300': !reason }"
2024-08-06 00:19:33 +03:00
required
2026-06-25 15:26:52 +03:00
:aria-label = "t('select_a_reason')"
2024-08-06 00:19:33 +03:00
:name = "`values[${field.preferences.reason_field_uuid}]`"
@change ="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
>
< option
value = ""
disabled
:selected = "!reason"
2024-10-10 19:49:31 +03:00
class = "text-gray-300"
2024-08-06 00:19:33 +03:00
>
{ { t ( 'select_a_reason' ) } }
< / option >
2025-11-11 13:42:01 +02:00
< template v-if = "field.preferences?.reasons" >
< option
v-for = "option in field.preferences.reasons"
:key = "option"
:value = "option"
: selected = "reason === option"
class = "text-base-content"
>
{ { option } }
< / option >
< / template >
< template v-else >
< option
v-for = "(label, option) in defaultReasons"
:key = "option"
:value = "option"
: selected = "reason === option"
class = "text-base-content"
>
{ { label } }
< / option >
< option
value = "other"
class = "text-base-content"
>
{ { t ( 'other' ) } }
< / option >
< / template >
2024-08-06 00:19:33 +03:00
< / select >
< input
2025-11-11 13:42:01 +02:00
v-if = "withSigningReason && isOtherReason"
2024-08-06 00:19:33 +03:00
class = "base-input !text-2xl w-full mt-6"
required
:name = "`values[${field.preferences.reason_field_uuid}]`"
2026-06-25 15:26:52 +03:00
:aria-label = "t('select_a_reason')"
2024-08-06 00:19:33 +03:00
:placeholder = "t('type_here_')"
:value = "reason"
type = "text"
@input ="$emit('update:reason', $event.target.value)"
>
< input
2025-11-11 13:42:01 +02:00
v-if = "withSigningReason"
2024-08-06 00:19:33 +03:00
hidden
name = "with_reason"
:value = "field.preferences.reason_field_uuid"
>
2024-05-12 12:08:18 +03:00
< div
2024-06-02 14:50:37 +03:00
v-if = "isShowQr"
dir = "auto"
class = "text-base-content/60 text-xs text-center w-full mt-1"
>
{ { t ( 'scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature' ) } }
< / div >
< div
v-else-if = "withDisclosure"
2024-05-12 12:08:18 +03:00
dir = "auto"
2025-12-15 10:22:27 +02:00
class = "text-base-content/60 text-xs text-center w-full mt-1 select-none"
2024-05-12 12:08:18 +03:00
>
{ { t ( 'by_clicking_you_agree_to_the' ) . replace ( '{button}' , buttonText . charAt ( 0 ) . toUpperCase ( ) + buttonText . slice ( 1 ) ) } } < a
2024-11-29 22:03:09 +02:00
href = "https://www.docuseal.com/esign-disclosure"
2024-05-12 12:08:18 +03:00
target = "_blank"
>
< span class = "inline md:hidden" >
{ { t ( 'esignature_disclosure' ) } }
< / span >
< span class = "hidden md:inline" >
{ { t ( 'electronic_signature_disclosure' ) } }
< / span >
< / a >
< / div >
2024-06-02 14:50:37 +03:00
< div
v-else
class = "mt-5 md:mt-7"
/ >
2023-05-25 22:47:52 +03:00
< / div >
< / template >
< script >
2024-06-02 14:50:37 +03:00
import { IconReload , IconCamera , IconSignature , IconTextSize , IconArrowsDiagonalMinimize2 , IconQrcode , IconX } from '@tabler/icons-vue'
2023-09-22 21:50:39 +03:00
import { cropCanvasAndExportToPNG } from './crop_canvas'
2026-02-21 09:53:03 +02:00
import { isValidSignatureCanvas , isCanvasBlocked } from './validate_signature'
2023-10-22 23:26:43 +03:00
import SignaturePad from 'signature_pad'
2024-01-06 17:21:15 +02:00
import AppearsOn from './appears_on'
2024-12-19 00:24:16 +02:00
import FileDropzone from './dropzone'
2024-03-09 18:24:21 +02:00
import MarkdownContent from './markdown_content'
2024-08-06 00:19:33 +03:00
import { v4 } from 'uuid'
2023-05-25 22:47:52 +03:00
2026-02-04 16:20:18 +02:00
let fontLoadPromise = null
2023-10-28 16:03:19 +03:00
2024-02-09 22:33:18 +02:00
const scale = 3
2023-05-25 22:47:52 +03:00
export default {
name : 'SignatureStep' ,
2023-06-15 01:05:04 +03:00
components : {
2024-01-06 17:21:15 +02:00
AppearsOn ,
2023-07-14 00:23:15 +03:00
IconReload ,
2024-12-19 00:24:16 +02:00
FileDropzone ,
2023-07-14 00:23:15 +03:00
IconCamera ,
2024-06-02 14:50:37 +03:00
IconQrcode ,
2024-03-09 18:24:21 +02:00
MarkdownContent ,
2024-06-02 14:50:37 +03:00
IconX ,
2023-07-17 22:48:01 +03:00
IconTextSize ,
2023-11-18 13:57:49 +02:00
IconSignature ,
2023-07-17 22:48:01 +03:00
IconArrowsDiagonalMinimize2
2023-06-15 01:05:04 +03:00
} ,
2023-09-20 00:13:14 +03:00
inject : [ 'baseUrl' , 't' ] ,
2023-05-25 22:47:52 +03:00
props : {
field : {
type : Object ,
required : true
} ,
2026-02-02 12:46:34 +02:00
values : {
type : Object ,
required : true
} ,
2024-08-06 00:19:33 +03:00
requireSigningReason : {
type : Boolean ,
required : false ,
default : false
} ,
2024-06-15 15:55:09 +03:00
submitter : {
type : Object ,
2023-05-25 22:47:52 +03:00
required : true
} ,
2024-01-24 23:20:53 +02:00
showFieldNames : {
type : Boolean ,
required : false ,
default : true
} ,
2024-07-02 20:27:25 +03:00
dryRun : {
type : Boolean ,
required : false ,
default : false
} ,
2024-05-12 12:08:18 +03:00
withDisclosure : {
type : Boolean ,
required : false ,
default : false
} ,
2024-06-02 14:50:37 +03:00
withQrButton : {
type : Boolean ,
required : false ,
default : false
} ,
2024-05-12 12:08:18 +03:00
buttonText : {
type : String ,
required : false ,
default : 'Submit'
} ,
2023-12-02 16:26:34 +02:00
withTypedSignature : {
type : Boolean ,
required : false ,
default : true
} ,
2024-06-15 15:55:09 +03:00
rememberSignature : {
type : Boolean ,
required : false ,
default : false
} ,
2023-05-25 22:47:52 +03:00
attachmentsIndex : {
type : Object ,
required : false ,
default : ( ) => ( { } )
} ,
2023-10-28 01:56:22 +03:00
previousValue : {
type : String ,
required : false ,
default : ''
} ,
2026-02-02 12:46:34 +02:00
touchAttachmentUuid : {
type : String ,
required : false ,
default : ''
} ,
2024-08-06 00:19:33 +03:00
reason : {
type : String ,
required : false ,
default : ''
} ,
2026-04-11 11:50:41 +03:00
signatureText : {
type : String ,
required : false ,
default : ''
} ,
signatureSrc : {
type : String ,
required : false ,
default : ''
} ,
2023-05-25 22:47:52 +03:00
modelValue : {
type : String ,
required : false ,
default : ''
}
} ,
2026-02-02 12:46:34 +02:00
emits : [ 'attached' , 'update:model-value' , 'start' , 'minimize' , 'update:reason' , 'touch-attachment' ] ,
2023-06-24 17:15:36 +03:00
data ( ) {
return {
2025-06-30 11:41:07 +03:00
isSignatureStarted : false ,
2024-06-02 14:50:37 +03:00
isShowQr : false ,
2024-08-06 00:19:33 +03:00
isOtherReason : false ,
2023-10-28 01:56:22 +03:00
isUsePreviousValue : true ,
2026-02-02 12:46:34 +02:00
isTouchAttachment : false ,
2026-04-11 11:50:41 +03:00
isTextSignature : ! this . signatureSrc && ( ! ! this . signatureText || this . field . preferences ? . format === 'typed' || this . field . preferences ? . format === 'typed_or_upload' ) ,
2024-02-18 22:12:28 +02:00
uploadImageInputKey : Math . random ( ) . toString ( )
2023-06-24 17:15:36 +03:00
}
} ,
2023-10-28 01:56:22 +03:00
computed : {
2024-06-15 15:55:09 +03:00
submitterSlug ( ) {
return this . submitter . slug
} ,
2024-12-19 00:24:16 +02:00
format ( ) {
return this . field . preferences ? . format
} ,
2025-11-11 13:42:01 +02:00
withSigningReason ( ) {
return this . requireSigningReason || this . field . preferences ? . reasons ? . length
} ,
2024-08-06 00:19:33 +03:00
defaultReasons ( ) {
return {
[ this . t ( 'approved_by' ) ] : this . t ( 'approved' ) ,
[ this . t ( 'reviewed_by' ) ] : this . t ( 'reviewed' ) ,
[ this . t ( 'authored_by' ) ] : this . t ( 'authored_by_me' )
}
} ,
2023-10-28 01:56:22 +03:00
computedPreviousValue ( ) {
2025-05-28 21:41:32 +03:00
if ( this . isUsePreviousValue && this . field . required === true ) {
2023-10-28 01:56:22 +03:00
return this . previousValue
} else {
return null
}
}
} ,
2024-08-06 00:19:33 +03:00
created ( ) {
2025-06-30 11:41:07 +03:00
this . isSignatureStarted = ! ! this . computedPreviousValue
2025-11-11 13:42:01 +02:00
if ( this . withSigningReason ) {
2024-08-06 00:19:33 +03:00
this . field . preferences || = { }
this . field . preferences . reason _field _uuid || = v4 ( )
2025-11-11 13:42:01 +02:00
this . isOtherReason = this . reason && ! this . defaultReasons [ this . reason ] &&
( ! this . field . preferences ? . reasons ? . length || ! this . field . preferences . reasons . includes ( this . reason ) )
2024-08-06 00:19:33 +03:00
}
} ,
2023-06-24 18:31:12 +03:00
async mounted ( ) {
2026-01-20 17:21:14 +02:00
this . $nextTick ( ( ) => this . setCanvasSize ( ) )
2023-06-15 01:05:04 +03:00
2023-09-20 00:13:14 +03:00
if ( this . $refs . canvas ) {
this . pad = new SignaturePad ( this . $refs . canvas )
2023-06-24 17:15:36 +03:00
2024-11-10 12:48:53 +02:00
if ( this . field . preferences ? . color ) {
this . pad . penColor = this . field . preferences . color
}
2024-06-06 16:54:16 +03:00
this . pad . addEventListener ( 'endStroke' , ( ) => {
2023-09-20 00:13:14 +03:00
this . isSignatureStarted = true
2023-07-17 22:48:01 +03:00
2023-09-20 00:13:14 +03:00
this . $emit ( 'start' )
} )
2024-02-07 01:47:57 +02:00
2026-01-20 17:21:14 +02:00
this . intersectionObserver = new IntersectionObserver ( ( entries ) => {
2024-02-07 01:47:57 +02:00
entries . forEach ( entry => {
if ( entry . isIntersecting ) {
2026-01-20 17:21:14 +02:00
this . setCanvasSize ( )
2024-02-08 12:11:10 +02:00
2026-04-11 11:50:41 +03:00
if ( this . signatureSrc ) {
this . $nextTick ( ( ) => this . drawSignatureSrc ( ) )
} else if ( this . isTextSignature ) {
2026-02-04 16:20:18 +02:00
this . $nextTick ( ( ) => {
2026-07-22 08:45:20 +03:00
if ( this . $refs . textInput && this . field . required === true ) {
2026-02-04 16:20:18 +02:00
this . initTypedSignature ( )
}
} )
}
2024-02-08 12:11:10 +02:00
this . intersectionObserver ? . disconnect ( )
2024-02-07 01:47:57 +02:00
}
} )
2024-02-08 12:11:10 +02:00
} )
this . intersectionObserver . observe ( this . $refs . canvas )
2026-01-20 17:21:14 +02:00
this . resizeObserver = new ResizeObserver ( ( ) => {
2026-01-26 12:06:41 +02:00
requestAnimationFrame ( ( ) => {
if ( ! this . $refs . canvas ) return
2026-04-30 14:51:24 +03:00
if ( ! this . $refs . canvas . parentNode ? . clientWidth ) return
2026-01-20 17:21:14 +02:00
2026-01-26 12:06:41 +02:00
const { width , height } = this . $refs . canvas
2026-01-20 17:21:14 +02:00
2026-01-26 12:06:41 +02:00
this . setCanvasSize ( )
if ( this . $refs . canvas . width !== width || this . $refs . canvas . height !== height ) {
this . redrawCanvas ( width , height )
}
} )
2026-01-20 17:21:14 +02:00
} )
this . resizeObserver . observe ( this . $refs . canvas . parentNode )
2026-02-04 16:20:18 +02:00
if ( this . isTextSignature ) {
this . loadFont ( )
}
2023-09-20 00:13:14 +03:00
}
2023-05-25 22:47:52 +03:00
} ,
2024-02-07 01:47:57 +02:00
beforeUnmount ( ) {
this . intersectionObserver ? . disconnect ( )
2026-01-20 17:21:14 +02:00
this . resizeObserver ? . disconnect ( )
2024-06-02 14:50:37 +03:00
this . stopCheckSignature ( )
2024-02-07 01:47:57 +02:00
} ,
2023-05-25 22:47:52 +03:00
methods : {
2026-01-20 17:21:14 +02:00
setCanvasSize ( ) {
const canvas = this . $refs . canvas
if ( canvas ) {
const width = canvas . parentNode . clientWidth
const height = width / 3
if ( canvas . width !== width * scale || canvas . height !== height * scale ) {
canvas . width = width * scale
canvas . height = height * scale
canvas . getContext ( '2d' ) . scale ( scale , scale )
}
}
} ,
redrawCanvas ( oldWidth , oldHeight ) {
const canvas = this . $refs . canvas
2026-04-30 14:42:22 +03:00
if ( this . pad && ! this . isTextSignature && ! this . pad . isEmpty ( ) && oldWidth > 0 && oldHeight > 0 && canvas . width > 0 && canvas . height > 0 ) {
2026-01-20 17:21:14 +02:00
const sx = canvas . width / oldWidth
const sy = canvas . height / oldHeight
const scaledData = this . pad . toData ( ) . map ( ( stroke ) => ( {
... stroke ,
points : stroke . points . map ( ( p ) => ( { ... p , x : p . x * sx , y : p . y * sy } ) )
} ) )
this . pad . fromData ( scaledData )
} else if ( this . isTextSignature && this . $refs . textInput ) {
2026-02-04 16:20:18 +02:00
this . updateWrittenSignature ( { target : this . $refs . textInput } )
2026-01-20 17:21:14 +02:00
}
} ,
2023-05-25 22:47:52 +03:00
remove ( ) {
this . $emit ( 'update:model-value' , '' )
2023-10-28 01:56:22 +03:00
this . isUsePreviousValue = false
this . isSignatureStarted = false
2023-05-25 22:47:52 +03:00
} ,
2023-10-28 16:03:19 +03:00
loadFont ( ) {
2026-02-04 16:20:18 +02:00
if ( ! fontLoadPromise ) {
2024-01-29 17:44:55 +02:00
const font = new FontFace ( 'Dancing Script' , ` url( ${ this . baseUrl } /fonts/DancingScript-Regular.otf) format("opentype") ` )
2023-10-28 16:03:19 +03:00
2026-02-04 16:20:18 +02:00
fontLoadPromise = font . load ( ) . then ( ( loadedFont ) => {
2023-10-28 16:03:19 +03:00
document . fonts . add ( loadedFont )
} ) . catch ( ( error ) => {
console . error ( 'Font loading failed:' , error )
} )
}
2026-02-04 16:20:18 +02:00
return fontLoadPromise
2023-10-28 16:03:19 +03:00
} ,
2024-06-02 14:50:37 +03:00
showQr ( ) {
this . isShowQr = true
2024-07-20 08:54:02 +03:00
this . $nextTick ( ( ) => {
import ( 'qr-creator' ) . then ( ( { default : Qr } ) => {
if ( this . $refs . qrCanvas ) {
Qr . render ( {
text : ` ${ document . location . origin } /p/ ${ this . submitterSlug } ?f= ${ this . field . uuid . split ( '-' ) [ 0 ] } ` ,
radius : 0.0 ,
ecLevel : 'H' ,
background : null ,
size : 132
} , this . $refs . qrCanvas )
}
} )
} )
2024-06-02 14:50:37 +03:00
this . startCheckSignature ( )
} ,
hideQr ( ) {
this . isShowQr = false
this . stopCheckSignature ( )
} ,
startCheckSignature ( ) {
const after = JSON . stringify ( new Date ( ) )
this . checkSignatureInterval = setInterval ( ( ) => {
this . checkSignature ( { after } )
} , 2000 )
} ,
stopCheckSignature ( ) {
if ( this . checkSignatureInterval ) {
clearInterval ( this . checkSignatureInterval )
}
} ,
checkSignature ( params = { } ) {
return fetch ( this . baseUrl + '/s/' + this . submitterSlug + '/values?field_uuid=' + this . field . uuid + '&after=' + params . after , {
method : 'GET'
} ) . then ( async ( resp ) => {
const { attachment } = await resp . json ( )
if ( attachment ? . uuid ) {
this . $emit ( 'attached' , attachment )
this . $emit ( 'update:model-value' , attachment . uuid )
this . hideQr ( )
}
} )
} ,
2023-05-25 22:47:52 +03:00
clear ( ) {
this . pad . clear ( )
2023-06-24 17:15:36 +03:00
this . isSignatureStarted = false
2023-07-14 00:23:15 +03:00
if ( this . $refs . textInput ) {
this . $refs . textInput . value = ''
2024-06-06 16:54:16 +03:00
this . $refs . textInput . focus ( )
2023-07-14 00:23:15 +03:00
}
} ,
updateWrittenSignature ( e ) {
2024-02-16 18:22:40 +02:00
this . isSignatureStarted = ! ! e . target . value
2023-07-14 00:23:15 +03:00
const canvas = this . $refs . canvas
const context = canvas . getContext ( '2d' )
2023-10-28 16:03:19 +03:00
const fontFamily = 'Dancing Script'
2024-07-06 00:52:12 +03:00
const initialFontSize = 44
2023-07-14 00:23:15 +03:00
const fontStyle = 'italic'
const fontWeight = ''
2024-07-06 00:52:12 +03:00
const setFontSize = ( size ) => {
context . font = ` ${ fontStyle } ${ fontWeight } ${ size } px ${ fontFamily } `
}
const adjustFontSizeToFit = ( text , maxWidth , initialSize ) => {
let size = initialSize
2023-07-14 00:23:15 +03:00
2024-07-06 00:52:12 +03:00
setFontSize ( size )
while ( context . measureText ( text ) . width > maxWidth && size > 1 ) {
size -= 1
setFontSize ( size )
}
}
adjustFontSizeToFit ( e . target . value , canvas . width / scale , initialFontSize )
context . textAlign = 'center'
2024-02-09 22:33:18 +02:00
context . clearRect ( 0 , 0 , canvas . width / scale , canvas . height / scale )
context . fillText ( e . target . value , canvas . width / 2 / scale , canvas . height / 2 / scale + 11 )
2023-07-14 00:23:15 +03:00
} ,
toggleTextInput ( ) {
2023-07-17 22:48:01 +03:00
this . remove ( )
2024-01-26 17:12:08 +02:00
this . clear ( )
2023-07-14 00:23:15 +03:00
this . isTextSignature = ! this . isTextSignature
2026-06-23 09:09:31 +03:00
this . $nextTick ( ( ) => {
if ( this . isTextSignature ) {
2026-02-04 16:20:18 +02:00
if ( this . $refs . textInput ) {
2026-06-23 09:09:31 +03:00
if ( this . submitter . name ) {
this . $refs . typeTextButton ? . focus ( )
} else {
2026-02-04 17:05:12 +02:00
this . $refs . textInput . focus ( )
}
2023-07-17 22:48:01 +03:00
2026-02-04 16:20:18 +02:00
this . initTypedSignature ( )
2023-10-28 16:03:19 +03:00
2026-02-04 16:20:18 +02:00
this . $emit ( 'start' )
}
2026-06-23 09:09:31 +03:00
} else {
this . $refs . typeTextButton ? . focus ( )
}
} )
2023-07-14 00:23:15 +03:00
} ,
2026-02-04 16:20:18 +02:00
async initTypedSignature ( ) {
2026-04-11 11:50:41 +03:00
if ( this . signatureText ) {
this . $refs . textInput . value = this . signatureText
} else if ( this . submitter . name ) {
2026-02-04 16:20:18 +02:00
this . $refs . textInput . value = this . submitter . name
}
await this . loadFont ( )
if ( this . $refs . textInput . value ) {
this . updateWrittenSignature ( { target : this . $refs . textInput } )
}
} ,
2026-04-11 11:50:41 +03:00
drawSignatureSrc ( ) {
const canvas = this . $refs . canvas
if ( ! canvas ) return
const img = new Image ( )
img . crossOrigin = 'anonymous'
img . onload = ( ) => {
const context = canvas . getContext ( '2d' )
const aspectRatio = img . width / img . height
const canvasWidth = canvas . width / scale
const canvasHeight = canvas . height / scale
let targetWidth = canvasWidth
let targetHeight = canvasHeight
if ( canvasWidth / canvasHeight > aspectRatio ) {
targetWidth = canvasHeight * aspectRatio
} else {
targetHeight = canvasWidth / aspectRatio
}
const x = ( canvasWidth - targetWidth ) / 2
const y = ( canvasHeight - targetHeight ) / 2
context . clearRect ( 0 , 0 , canvasWidth , canvasHeight )
context . drawImage ( img , x , y , targetWidth , targetHeight )
this . isSignatureStarted = true
this . $emit ( 'start' )
}
img . onerror = ( ) => {
console . error ( ` Failed to load signature image from ${ this . signatureSrc } . The remote server must send an Access-Control-Allow-Origin header to allow CORS access. ` )
}
img . src = this . signatureSrc
} ,
2023-07-14 00:23:15 +03:00
drawImage ( event ) {
2023-07-17 22:48:01 +03:00
this . remove ( )
2025-03-29 15:44:19 +02:00
this . clear ( )
2023-07-14 00:23:15 +03:00
this . isSignatureStarted = true
2024-08-20 16:15:49 +03:00
this . drawOnCanvas ( event . target . files [ 0 ] , this . $refs . canvas )
2024-08-19 21:35:26 +03:00
this . uploadImageInputKey = Math . random ( ) . toString ( )
} ,
2024-08-20 16:15:49 +03:00
drawOnCanvas ( file , canvas ) {
2023-07-14 00:23:15 +03:00
if ( file && file . type . match ( 'image.*' ) ) {
const reader = new FileReader ( )
reader . onload = ( event ) => {
const img = new Image ( )
img . src = event . target . result
img . onload = ( ) => {
const context = canvas . getContext ( '2d' )
const aspectRatio = img . width / img . height
2024-02-09 22:33:18 +02:00
const canvasWidth = canvas . width / scale
const canvasHeight = canvas . height / scale
2023-07-14 00:23:15 +03:00
2024-02-09 22:33:18 +02:00
let targetWidth = canvasWidth
let targetHeight = canvasHeight
2023-07-14 00:23:15 +03:00
2024-02-09 22:33:18 +02:00
if ( canvasWidth / canvasHeight > aspectRatio ) {
targetWidth = canvasHeight * aspectRatio
2023-07-14 00:23:15 +03:00
} else {
2024-02-09 22:33:18 +02:00
targetHeight = canvasWidth / aspectRatio
2023-07-14 00:23:15 +03:00
}
if ( targetHeight > targetWidth ) {
const scale = targetHeight / targetWidth
targetWidth = targetWidth * scale
targetHeight = targetHeight * scale
}
2024-02-09 22:33:18 +02:00
const x = ( canvasWidth - targetWidth ) / 2
const y = ( canvasHeight - targetHeight ) / 2
2023-07-14 00:23:15 +03:00
2024-02-20 00:59:52 +02:00
setTimeout ( ( ) => {
context . clearRect ( 0 , 0 , canvasWidth , canvasHeight )
context . drawImage ( img , x , y , targetWidth , targetHeight )
2023-07-17 22:48:01 +03:00
2024-02-20 00:59:52 +02:00
this . $emit ( 'start' )
} , 50 )
2023-07-14 00:23:15 +03:00
}
}
reader . readAsDataURL ( file )
}
} ,
2024-06-15 15:55:09 +03:00
maybeSetSignedUuid ( signedUuid ) {
try {
if ( window . localStorage && signedUuid && this . rememberSignature ) {
const values = window . localStorage . getItem ( 'signed_signature_uuids' )
let data
if ( values ) {
data = JSON . parse ( values )
} else {
data = { }
}
data [ this . submitter . email ] = signedUuid
window . localStorage . setItem ( 'signed_signature_uuids' , JSON . stringify ( data ) )
}
} catch ( e ) {
console . error ( e )
}
} ,
2023-06-24 18:31:12 +03:00
async submit ( ) {
2023-10-28 01:56:22 +03:00
if ( this . modelValue || this . computedPreviousValue ) {
2026-02-02 12:46:34 +02:00
if ( this . touchAttachmentUuid && this . computedPreviousValue === this . touchAttachmentUuid && ! Object . values ( this . values ) . includes ( this . touchAttachmentUuid ) ) {
this . isTouchAttachment = true
this . $emit ( 'touch-attachment' , this . touchAttachmentUuid )
} else {
this . isTouchAttachment = false
}
2023-10-28 01:56:22 +03:00
if ( this . computedPreviousValue ) {
this . $emit ( 'update:model-value' , this . computedPreviousValue )
}
2023-05-25 22:47:52 +03:00
return Promise . resolve ( { } )
}
2025-03-29 13:40:25 +02:00
if ( this . isSignatureStarted && this . pad . toData ( ) . length > 0 && ! isValidSignatureCanvas ( this . pad . toData ( ) ) ) {
2025-04-17 22:06:27 +03:00
if ( this . field . required === true || this . pad . toData ( ) . length > 0 ) {
2026-02-21 09:53:03 +02:00
if ( isCanvasBlocked ( ) ) {
alert ( this . t ( 'browser_privacy_settings_block_canvas' ) )
if ( window . Rollbar ) {
window . Rollbar . info ( 'Canvas blocked' )
}
} else {
alert ( this . t ( 'signature_is_too_small_or_simple_please_redraw' ) )
}
2025-03-29 13:40:25 +02:00
2025-04-17 22:06:27 +03:00
return Promise . reject ( new Error ( 'Image too small or simple' ) )
} else {
Promise . resolve ( { } )
}
2025-03-29 13:40:25 +02:00
}
2025-04-17 22:06:27 +03:00
return new Promise ( ( resolve , reject ) => {
cropCanvasAndExportToPNG ( this . $refs . canvas , { errorOnTooSmall : true } ) . then ( async ( blob ) => {
2023-05-25 22:47:52 +03:00
const file = new File ( [ blob ] , 'signature.png' , { type : 'image/png' } )
2024-07-02 20:27:25 +03:00
if ( this . dryRun ) {
const reader = new FileReader ( )
2024-04-06 15:00:30 +03:00
2024-07-02 20:27:25 +03:00
reader . readAsDataURL ( file )
2024-04-06 15:00:30 +03:00
2024-07-02 20:27:25 +03:00
reader . onloadend = ( ) => {
const attachment = { url : reader . result , uuid : Math . random ( ) . toString ( ) }
2024-04-06 15:00:30 +03:00
2024-07-02 20:27:25 +03:00
this . $emit ( 'attached' , attachment )
this . $emit ( 'update:model-value' , attachment . uuid )
2024-06-15 15:55:09 +03:00
2024-07-02 20:27:25 +03:00
resolve ( attachment )
}
} else {
const formData = new FormData ( )
formData . append ( 'file' , file )
formData . append ( 'submitter_slug' , this . submitterSlug )
formData . append ( 'name' , 'attachments' )
formData . append ( 'remember_signature' , this . rememberSignature )
2025-06-26 11:04:25 +03:00
formData . append ( 'type' , 'signature' )
2024-07-02 20:27:25 +03:00
return fetch ( this . baseUrl + '/api/attachments' , {
method : 'POST' ,
body : formData
2025-06-26 11:04:25 +03:00
} ) . then ( async ( resp ) => {
if ( resp . status === 422 || resp . status === 500 ) {
const data = await resp . json ( )
return Promise . reject ( new Error ( data . error ) )
}
const attachment = await resp . json ( )
2024-07-02 20:27:25 +03:00
this . $emit ( 'attached' , attachment )
this . $emit ( 'update:model-value' , attachment . uuid )
this . maybeSetSignedUuid ( attachment . signed _uuid )
return resolve ( attachment )
} )
}
2025-04-17 22:06:27 +03:00
} ) . catch ( ( error ) => {
if ( this . field . required === true ) {
2026-02-21 09:53:03 +02:00
if ( isCanvasBlocked ( ) ) {
alert ( this . t ( 'browser_privacy_settings_block_canvas' ) )
if ( window . Rollbar ) {
window . Rollbar . info ( 'Canvas blocked' )
}
} else {
alert ( this . t ( 'signature_is_too_small_or_simple_please_redraw' ) )
}
2025-04-17 22:06:27 +03:00
return reject ( error )
} else {
return resolve ( { } )
}
2023-07-14 00:23:15 +03:00
} )
2023-05-25 22:47:52 +03:00
} )
}
}
}
< / script >