Signature block (img) (#39)

* Signature block (img)

* Singature input UI changes

* Style the signature input

Co-authored-by: Julien Nahum <jhumanj@MacBook-Pro-de-Julien.local>
This commit is contained in:
Chirag 2022-12-22 16:23:33 +05:30 committed by GitHub
parent 9936cb3389
commit 07f44ec048
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 141 additions and 1 deletions

View File

@ -124,6 +124,7 @@ class AnswerFormRequest extends FormRequest
switch ($property['type']) { switch ($property['type']) {
case 'text': case 'text':
case 'phone_number': case 'phone_number':
case 'signature':
return ['string']; return ['string'];
case 'number': case 'number':
return ['numeric']; return ['numeric'];

View File

@ -41,7 +41,7 @@ class FormSubmissionResource extends JsonResource
$data = $this->data; $data = $this->data;
$formFields = collect($this->form->properties)->concat(collect($this->form->removed_properties)); $formFields = collect($this->form->properties)->concat(collect($this->form->removed_properties));
$fileFields = $formFields->filter(function ($field) { $fileFields = $formFields->filter(function ($field) {
return $field['type'] == 'files'; return in_array($field['type'], ['files', 'signature']);
}); });
foreach ($fileFields as $field) { foreach ($fileFields as $field) {
if (isset($data[$field['id']]) && !empty($data[$field['id']])) { if (isset($data[$field['id']]) && !empty($data[$field['id']])) {

View File

@ -89,6 +89,11 @@ class StoreFormSubmissionJob implements ShouldQueue
} }
} }
} }
// For Singrature
if($this->form->is_pro && $field['type'] == 'signature') {
$finalData[$field['id']] = $this->storeSignature($answerValue);
}
} }
return $finalData; return $finalData;
@ -130,6 +135,21 @@ class StoreFormSubmissionJob implements ShouldQueue
return $fileNameParser->getMovedFileName(); return $fileNameParser->getMovedFileName();
} }
private function storeSignature(?string $value)
{
if ($value == null || !isset(explode(',', $value)[1])) {
return null;
}
$fileName = 'sign_'.(string) Str::uuid().'.png';
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
$completeNewFilename = $newPath.'/'.$fileName;
Storage::disk('s3')->put($completeNewFilename, base64_decode(explode(',', $value)[1]));
return $fileName;
}
/** /**
* Adds prefill from hidden fields * Adds prefill from hidden fields
* *

44
package-lock.json generated
View File

@ -32,6 +32,7 @@
"vue-plugin-load-script": "^1.3.2", "vue-plugin-load-script": "^1.3.2",
"vue-prism-editor": "^1.2.2", "vue-prism-editor": "^1.2.2",
"vue-router": "^3.5.2", "vue-router": "^3.5.2",
"vue-signature-pad": "^2.0.5",
"vue-tailwind": "^2.5.0", "vue-tailwind": "^2.5.0",
"vue2-editor": "^2.10.3", "vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
@ -10616,6 +10617,11 @@
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
"dev": true "dev": true
}, },
"node_modules/merge-images": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/merge-images/-/merge-images-1.2.0.tgz",
"integrity": "sha512-hEGvgnTdXr08uzGvEArxRsKpy7WmozM73YaSi4s5IYF4LxrhANpqfHaz9CgBZ5+0+s2NsjPnPdStz3aCc0Yulw=="
},
"node_modules/merge-source-map": { "node_modules/merge-source-map": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
@ -14291,6 +14297,11 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true "dev": true
}, },
"node_modules/signature_pad": {
"version": "3.0.0-beta.4",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-3.0.0-beta.4.tgz",
"integrity": "sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw=="
},
"node_modules/slash": { "node_modules/slash": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@ -16364,6 +16375,19 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz",
"integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ=="
}, },
"node_modules/vue-signature-pad": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/vue-signature-pad/-/vue-signature-pad-2.0.5.tgz",
"integrity": "sha512-FLvJRmhSP/DVkg0W/tGcPZ4D0ZQlFxHgN1mjph6jt/59yLeTSv0zGGU133lfq9CkYMQ5DqN6rWujjL7Gnx7UPA==",
"dependencies": {
"merge-images": "^1.1.0",
"signature_pad": "^3.0.0-beta.3",
"vue": "^2.6.14"
},
"engines": {
"node": ">=12"
}
},
"node_modules/vue-style-loader": { "node_modules/vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -25446,6 +25470,11 @@
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
"dev": true "dev": true
}, },
"merge-images": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/merge-images/-/merge-images-1.2.0.tgz",
"integrity": "sha512-hEGvgnTdXr08uzGvEArxRsKpy7WmozM73YaSi4s5IYF4LxrhANpqfHaz9CgBZ5+0+s2NsjPnPdStz3aCc0Yulw=="
},
"merge-source-map": { "merge-source-map": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
@ -28183,6 +28212,11 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true "dev": true
}, },
"signature_pad": {
"version": "3.0.0-beta.4",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-3.0.0-beta.4.tgz",
"integrity": "sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw=="
},
"slash": { "slash": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@ -29801,6 +29835,16 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz",
"integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ=="
}, },
"vue-signature-pad": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/vue-signature-pad/-/vue-signature-pad-2.0.5.tgz",
"integrity": "sha512-FLvJRmhSP/DVkg0W/tGcPZ4D0ZQlFxHgN1mjph6jt/59yLeTSv0zGGU133lfq9CkYMQ5DqN6rWujjL7Gnx7UPA==",
"requires": {
"merge-images": "^1.1.0",
"signature_pad": "^3.0.0-beta.3",
"vue": "^2.6.14"
}
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

View File

@ -38,6 +38,7 @@
"vue-plugin-load-script": "^1.3.2", "vue-plugin-load-script": "^1.3.2",
"vue-prism-editor": "^1.2.2", "vue-prism-editor": "^1.2.2",
"vue-router": "^3.5.2", "vue-router": "^3.5.2",
"vue-signature-pad": "^2.0.5",
"vue-tailwind": "^2.5.0", "vue-tailwind": "^2.5.0",
"vue2-editor": "^2.10.3", "vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",

View File

@ -0,0 +1,53 @@
<template>
<div :class="wrapperClass">
<label v-if="label" :for="id?id:name"
:class="[theme.CodeInput.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
>
{{ label }}
<span v-if="required" class="text-red-500 required-dot">*</span>
</label>
<VueSignaturePad ref="signaturePad"
:class="[theme.default.input,{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name), 'cursor-not-allowed bg-gray-200':disabled }]" height="150px"
:name="name"
:options="{ onEnd }"
/>
<div class="flex">
<small v-if="help" :class="theme.default.help" class="flex-grow">
<slot name="help"><span class="field-help" v-html="help" /></slot>
</small>
<small v-else class="flex-grow" />
<small :class="theme.default.help">
<a :class="theme.default.help" href="#" @click.prevent="clear">Clear</a>
</small>
</div>
<has-error v-if="hasValidation" :form="form" :field="name" />
</div>
</template>
<script>
import Vue from 'vue'
import VueSignaturePad from 'vue-signature-pad'
import inputMixin from '~/mixins/forms/input'
Vue.use(VueSignaturePad)
export default {
name: 'SignatureInput',
components: {},
mixins: [inputMixin],
methods: {
clear () {
this.$refs.signaturePad.clearSignature()
this.onEnd()
},
onEnd () {
const { isEmpty, data } = this.$refs.signaturePad.saveSignature()
this.$set(this.form, this.name, (!isEmpty && data) ? data : null)
}
}
}
</script>

View File

@ -38,6 +38,7 @@ import ToggleSwitchInput from './ToggleSwitchInput'
}) })
// Lazy load some heavy component // Lazy load some heavy component
Vue.component('SignatureInput', () => import('./SignatureInput'))
Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput')) Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput'))
Vue.component('DateInput', () => import('./DateInput')) Vue.component('DateInput', () => import('./DateInput'))
Vue.component('SimpleDateInput', () => import('./SimpleDateInput')) Vue.component('SimpleDateInput', () => import('./SimpleDateInput'))

View File

@ -348,6 +348,9 @@ export default {
if (field.type === 'checkbox' && field.use_toggle_switch) { if (field.type === 'checkbox' && field.use_toggle_switch) {
return 'ToggleSwitchInput' return 'ToggleSwitchInput'
} }
if (field.type === 'signature') {
return 'SignatureInput'
}
if (field.type === 'date' && field.simple_date_input) { if (field.type === 'date' && field.simple_date_input) {
return 'SimpleDateInput' return 'SimpleDateInput'
} }

View File

@ -143,6 +143,19 @@
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">File Input</p> <p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">File Input</p>
</div> </div>
<!-- Signature Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('signature')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Signature Input</p>
</div>
</div> </div>
<p class="text-gray-500 uppercase text-xs font-semibold mb-2 mt-6">Layout Blocks</p> <p class="text-gray-500 uppercase text-xs font-semibold mb-2 mt-6">Layout Blocks</p>
@ -250,6 +263,7 @@ export default {
'select': 'Select', 'select': 'Select',
'multi_select': 'Multi Select', 'multi_select': 'Multi Select',
'files': 'Files', 'files': 'Files',
'signature': 'Signature',
'nf-text': 'Text Block', 'nf-text': 'Text Block',
'nf-page-break': 'Page Break', 'nf-page-break': 'Page Break',
'nf-divider': 'Divider', 'nf-divider': 'Divider',
@ -307,6 +321,8 @@ export default {
data.previous_btn_text = 'Previous' data.previous_btn_text = 'Previous'
} else if (data.type === 'nf-code') { } else if (data.type === 'nf-code') {
data.content = '<div class="text-blue-500 italic">This is a code block.</div>' data.content = '<div class="text-blue-500 italic">This is a code block.</div>'
} else if (data.type === 'signature') {
data.help = 'Draw your signature above'
} }
return data return data
}, },

View File

@ -143,6 +143,7 @@ export default {
url: OpenUrl, url: OpenUrl,
email: OpenText, email: OpenText,
phone_number: OpenText, phone_number: OpenText,
signature: OpenFile,
} }
}, },
}, },