From f6c061a57bc997bf060c3b2db7dc9d355d8c6355 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 2 Jun 2024 14:50:37 +0300 Subject: [PATCH] allow to draw signature on mobile --- .../submit_form_draw_signature_controller.rb | 21 +++ .../submit_form_values_controller.rb | 21 +++ app/javascript/draw.js | 113 +++++++++++++++ app/javascript/form.js | 1 + app/javascript/submission_form/form.vue | 8 +- app/javascript/submission_form/i18n.js | 26 ++++ .../submission_form/signature_step.vue | 131 ++++++++++++++++-- .../submit_form_draw_signature/show.html.erb | 51 +++++++ config/routes.rb | 3 + config/webpack/webpack.config.js | 4 + package.json | 1 + yarn.lock | 5 + 12 files changed, 374 insertions(+), 11 deletions(-) create mode 100644 app/controllers/submit_form_draw_signature_controller.rb create mode 100644 app/controllers/submit_form_values_controller.rb create mode 100644 app/javascript/draw.js create mode 100644 app/views/submit_form_draw_signature/show.html.erb diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb new file mode 100644 index 00000000..f8352ade --- /dev/null +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class SubmitFormDrawSignatureController < ApplicationController + layout false + + around_action :with_browser_locale, only: %i[show] + skip_before_action :authenticate_user! + skip_authorization_check + + def show + @submitter = Submitter.find_by!(slug: params[:slug]) + + return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? + + if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? + return redirect_to submit_form_path(@submitter.slug) + end + + render :show + end +end diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb new file mode 100644 index 00000000..ecc21817 --- /dev/null +++ b/app/controllers/submit_form_values_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class SubmitFormValuesController < ApplicationController + skip_before_action :authenticate_user! + skip_authorization_check + + def index + submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + + return render json: {} if submitter.completed_at? + return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at? + + value = submitter.values[params['field_uuid']] + attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? + + render json: { + value:, + attachment: attachment&.as_json(only: %i[uuid], methods: %i[url filename content_type]) + }, head: :ok + end +end diff --git a/app/javascript/draw.js b/app/javascript/draw.js new file mode 100644 index 00000000..916bd4dc --- /dev/null +++ b/app/javascript/draw.js @@ -0,0 +1,113 @@ +import SignaturePad from 'signature_pad' +import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas' + +window.customElements.define('signature-form', class extends HTMLElement { + connectedCallback () { + const scale = 3 + + this.canvas.width = this.canvas.parentNode.clientWidth * scale + this.canvas.height = this.canvas.parentNode.clientHeight * scale + + this.canvas.getContext('2d').scale(scale, scale) + + this.pad = new SignaturePad(this.canvas) + + this.pad.addEventListener('endStroke', () => { + this.updateSubmitButtonVisibility() + }) + + this.clearButton.addEventListener('click', (e) => { + e.preventDefault() + + this.clearSignaturePad() + }) + + this.form.addEventListener('submit', (e) => { + e.preventDefault() + + this.submitButton.disabled = true + + this.submitImage().then((data) => { + this.valueInput.value = data.uuid + + return fetch(this.form.action, { + method: 'PUT', + body: new FormData(this.form) + }).then((response) => { + this.form.classList.add('hidden') + this.success.classList.remove('hidden') + + return response + }) + }).finally(() => { + this.submitButton.disabled = false + }) + }) + } + + clearSignaturePad () { + this.pad.clear() + this.updateSubmitButtonVisibility() + } + + updateSubmitButtonVisibility () { + if (this.pad.isEmpty()) { + this.submitButton.style.display = 'none' + this.placeholderButton.style.display = 'block' + } else { + this.submitButton.style.display = 'block' + this.placeholderButton.style.display = 'none' + } + } + + async submitImage () { + return new Promise((resolve, reject) => { + cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => { + const file = new File([blob], 'signature.png', { type: 'image/png' }) + + const formData = new FormData() + + formData.append('file', file) + formData.append('submitter_slug', this.dataset.slug) + formData.append('name', 'attachments') + + return fetch('/api/attachments', { + method: 'POST', + body: formData + }).then((resp) => resp.json()).then((attachment) => { + return resolve(attachment) + }) + }).catch((error) => { + return reject(error) + }) + }) + } + + get submitButton () { + return this.querySelector('button[type="submit"]') + } + + get clearButton () { + return this.querySelector('button[aria-label="Clear"]') + } + + get placeholderButton () { + return this.querySelector('button[disabled]') + } + + get canvas () { + return this.querySelector('canvas') + } + + get valueInput () { + return this.querySelector('input[name^="values"]') + } + + get form () { + return this.querySelector('form') + } + + get success () { + return this.querySelector('#success') + } +}) diff --git a/app/javascript/form.js b/app/javascript/form.js index a818def5..1f15737b 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -24,6 +24,7 @@ safeRegisterElement('submission-form', class extends HTMLElement { authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, values: reactive(JSON.parse(this.dataset.values)), completedButton: JSON.parse(this.dataset.completedButton || '{}'), + withQrButton: true, completedMessage: JSON.parse(this.dataset.completedMessage || '{}'), completedRedirectUrl: this.dataset.completedRedirectUrl, attachments: reactive(JSON.parse(this.dataset.attachments)), diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index c8250167..46815f92 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -326,6 +326,7 @@ :attachments-index="attachmentsIndex" :button-text="buttonText" :with-disclosure="withDisclosure" + :with-qr-button="withQrButton" :submitter-slug="submitterSlug" :show-field-names="showFieldNames" @attached="attachments.push($event)" @@ -384,7 +385,7 @@