From d33611d6c70192d0172a5f78864564fb50a8a1d6 Mon Sep 17 00:00:00 2001 From: keerthi-balaji Date: Thu, 27 Jun 2024 23:16:54 +0530 Subject: [PATCH 1/4] Added signature pad element and added styling for it before and after render --- src/js/control/index.js | 2 + src/js/control/signaturePad.js | 88 ++++++++++++++++++++++++++++++++++ src/sass/_controls.scss | 16 +++++++ src/sass/form-render.scss | 17 +++++++ 4 files changed, 123 insertions(+) create mode 100644 src/js/control/signaturePad.js diff --git a/src/js/control/index.js b/src/js/control/index.js index 21de29d73..d6baeca45 100644 --- a/src/js/control/index.js +++ b/src/js/control/index.js @@ -8,6 +8,7 @@ import controlText from './text' import controlTextarea from './textarea' import controlTinymce from './textarea.tinymce' import controlQuill from './textarea.quill' +import controlSignaturePad from './signaturePad' export default { controlAutocomplete, @@ -20,4 +21,5 @@ export default { controlTextarea, controlTinymce, controlQuill, + controlSignaturePad, } diff --git a/src/js/control/signaturePad.js b/src/js/control/signaturePad.js new file mode 100644 index 000000000..c8f081b7e --- /dev/null +++ b/src/js/control/signaturePad.js @@ -0,0 +1,88 @@ +import control from '../control' + +/** + * SignaturePad class + * @extends control + */ +export default class controlSignaturePad extends control { + /** + * definition + * @return {Object} + */ + static get definition() { + return { + icon: '🖊️', + i18n: { + default: 'Signature Pad', + }, + } + } + + /** + * build a signature pad DOM element + * @return {Object} DOM Element to be injected into the form. + */ + build() { + this.canvas = this.markup('canvas', null, { className: 'signature-pad' }) + this.clearButton = this.markup('button', 'Clear', { type: 'button', className: 'clear-button' }) + this.clearButton.addEventListener('click', () => { + const context = this.canvas.getContext('2d') + context.clearRect(0, 0, this.canvas.width, this.canvas.height) + + // Clear any existing drawing data + this.clearCanvas() + + }) + this.labelSpan = this.markup('span', this.config.label || 'Signature', { className: 'form-label' }) + + // Created a container div and wrap the canvas inside it + const container = this.markup('div', [this.canvas], { className: 'signature-container' }) + + return [this.labelSpan, container, this.clearButton] + + } + + /** + * Clear the canvas and any existing drawing data + */ + clearCanvas() { + const context = this.canvas.getContext('2d') + + context.beginPath() // Start a new path to clear previous strokes + + context.clearRect(0, 0, this.canvas.width, this.canvas.height) + } + + /** + * onRender callback + * @param {Object} evt Event object + */ + onRender(evt) { + this.canvas.width = this.canvas.parentElement.offsetWidth + this.canvas.height = 150 // Fixed height for the canvas + + const context = this.canvas.getContext('2d') + context.strokeStyle = '#000' + context.lineWidth = 2 + + let isDrawing = false + this.canvas.addEventListener('mousedown', e => { + isDrawing = true + context.moveTo(e.offsetX, e.offsetY) + }) + this.canvas.addEventListener('mousemove', e => { + if (isDrawing) { + context.lineTo(e.offsetX, e.offsetY) + context.stroke() + } + }) + this.canvas.addEventListener('mouseup', () => { + isDrawing = false + }) + this.canvas.addEventListener('mouseout', () => { + isDrawing = false + }) + return evt + } +} +control.register('signaturePad', controlSignaturePad) \ No newline at end of file diff --git a/src/sass/_controls.scss b/src/sass/_controls.scss index cc8b417e2..d38d652e0 100644 --- a/src/sass/_controls.scss +++ b/src/sass/_controls.scss @@ -178,3 +178,19 @@ } } } +.signature-container { + border: 2px solid #000; + padding: 10px; + margin-bottom: 10px; + border-radius: 5px; +} + +.signature-pad { + //display: block; + width: 100%; + height: 100%; +} +.signature-pad.custom-style { + border: 2px solid #000; + background-color: #f9f9f9; +} diff --git a/src/sass/form-render.scss b/src/sass/form-render.scss index 1c6ea2d2a..008589efa 100644 --- a/src/sass/form-render.scss +++ b/src/sass/form-render.scss @@ -46,4 +46,21 @@ height: auto; } } + .signature-container { + border: 2px solid #000; + padding: 10px; + margin-bottom: 10px; + border-radius: 5px; + } + + .signature-pad { + //display: block; + width: 100%; + height: 100%; + } + .signature-pad.custom-style { + border: 2px solid #000; + background-color: #f9f9f9; + } + } \ No newline at end of file From 4ed922c4180f08a5b2f2957943d4ed0645054e04 Mon Sep 17 00:00:00 2001 From: keerthi-balaji Date: Fri, 12 Jul 2024 02:12:38 +0530 Subject: [PATCH 2/4] Fixed the label, placed the clear button within the canvas, renamed the button to 'clear signature', and styled the button to match the background --- src/js/control/signaturePad.js | 12 +++++----- src/sass/_controls.scss | 11 +++++++-- src/sass/form-render.scss | 41 ++++++++++++++++++++-------------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/js/control/signaturePad.js b/src/js/control/signaturePad.js index c8f081b7e..9b370f616 100644 --- a/src/js/control/signaturePad.js +++ b/src/js/control/signaturePad.js @@ -24,7 +24,7 @@ export default class controlSignaturePad extends control { */ build() { this.canvas = this.markup('canvas', null, { className: 'signature-pad' }) - this.clearButton = this.markup('button', 'Clear', { type: 'button', className: 'clear-button' }) + this.clearButton = this.markup('button', 'Clear Signature', { type: 'button', className: 'clear-button' }) this.clearButton.addEventListener('click', () => { const context = this.canvas.getContext('2d') context.clearRect(0, 0, this.canvas.width, this.canvas.height) @@ -33,12 +33,12 @@ export default class controlSignaturePad extends control { this.clearCanvas() }) - this.labelSpan = this.markup('span', this.config.label || 'Signature', { className: 'form-label' }) + this.labelSpan = this.markup('span', this.config.label, { className: 'form-label' }) - // Created a container div and wrap the canvas inside it - const container = this.markup('div', [this.canvas], { className: 'signature-container' }) + // Created a container div and wrap the canvas and clear button inside it + const container = this.markup('div', [this.canvas, this.clearButton], { className: 'signature-container' }) - return [this.labelSpan, container, this.clearButton] + return [this.labelSpan, container] } @@ -51,6 +51,7 @@ export default class controlSignaturePad extends control { context.beginPath() // Start a new path to clear previous strokes context.clearRect(0, 0, this.canvas.width, this.canvas.height) + } /** @@ -84,5 +85,6 @@ export default class controlSignaturePad extends control { }) return evt } + } control.register('signaturePad', controlSignaturePad) \ No newline at end of file diff --git a/src/sass/_controls.scss b/src/sass/_controls.scss index d38d652e0..6b5caa6aa 100644 --- a/src/sass/_controls.scss +++ b/src/sass/_controls.scss @@ -184,9 +184,7 @@ margin-bottom: 10px; border-radius: 5px; } - .signature-pad { - //display: block; width: 100%; height: 100%; } @@ -194,3 +192,12 @@ border: 2px solid #000; background-color: #f9f9f9; } +.clear-button { + top: 10px; /* Adjust as needed */ + right: 10px; /* Adjust as needed */ + right: 10px; + background-color: #f8f8f8; + border: 1px solid #ccc; + padding: 5px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/sass/form-render.scss b/src/sass/form-render.scss index 008589efa..3780c35f8 100644 --- a/src/sass/form-render.scss +++ b/src/sass/form-render.scss @@ -46,21 +46,28 @@ height: auto; } } - .signature-container { - border: 2px solid #000; - padding: 10px; - margin-bottom: 10px; - border-radius: 5px; - } - - .signature-pad { - //display: block; - width: 100%; - height: 100%; - } - .signature-pad.custom-style { - border: 2px solid #000; - background-color: #f9f9f9; - } - +.signature-container { + border: 2px solid #000; + padding: 10px; + margin-bottom: 10px; + border-radius: 5px; +} + +.signature-pad { + width: 100%; + height: 100%; +} +.signature-pad.custom-style { + border: 2px solid #000; + background-color: #f9f9f9; +} +.clear-button { + top: 10px; /* Adjust as needed */ + right: 10px; /* Adjust as needed */ + right: 10px; + background-color: #f8f8f8; + border: 1px solid #ccc; + padding: 5px; + cursor: pointer; +} } \ No newline at end of file From e0bb509b0cb7dcef06598832ea7f7a7611375400 Mon Sep 17 00:00:00 2001 From: keerthi-balaji Date: Fri, 12 Jul 2024 22:59:43 +0530 Subject: [PATCH 3/4] Added a method to stop signature pad from being dragged around prerende --- src/js/control/signaturePad.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/js/control/signaturePad.js b/src/js/control/signaturePad.js index 9b370f616..41190442a 100644 --- a/src/js/control/signaturePad.js +++ b/src/js/control/signaturePad.js @@ -86,5 +86,25 @@ export default class controlSignaturePad extends control { return evt } + /** + * extend the default events to add a prerender for the signature pad + * @param {string} eventType + * @return {Function} prerender function + */ + on(eventType) { + if (eventType === 'prerender' && this.preview) { + return element => { + if (this.field) { + element = this.field + } + + // if this is a preview, stop events bubbling up so the editor preview is clickable (and not draggable) + $(element).on('mousedown', e => { + e.stopPropagation() + }) + } + } + return super.on(eventType) + } } control.register('signaturePad', controlSignaturePad) \ No newline at end of file From e3efb045b82956701a521ce8b7b5ddd1fcd324a8 Mon Sep 17 00:00:00 2001 From: keerthi-balaji Date: Fri, 23 Aug 2024 00:34:40 -0400 Subject: [PATCH 4/4] Saved the signature to form data and created the tests --- src/js/control/signaturePad.js | 131 ++++++++++++++++++----------- tests/control/signaturePad.test.js | 100 ++++++++++++++++++++++ 2 files changed, 183 insertions(+), 48 deletions(-) create mode 100644 tests/control/signaturePad.test.js diff --git a/src/js/control/signaturePad.js b/src/js/control/signaturePad.js index 41190442a..d5a981136 100644 --- a/src/js/control/signaturePad.js +++ b/src/js/control/signaturePad.js @@ -5,13 +5,18 @@ import control from '../control' * @extends control */ export default class controlSignaturePad extends control { + constructor(config) { + super(config) + this.userData = config.userData || null + } + /** * definition * @return {Object} */ static get definition() { return { - icon: '🖊️', + icon: '🖊️', i18n: { default: 'Signature Pad', }, @@ -28,18 +33,18 @@ export default class controlSignaturePad extends control { this.clearButton.addEventListener('click', () => { const context = this.canvas.getContext('2d') context.clearRect(0, 0, this.canvas.width, this.canvas.height) - - // Clear any existing drawing data this.clearCanvas() - }) + this.labelSpan = this.markup('span', this.config.label, { className: 'form-label' }) - // Created a container div and wrap the canvas and clear button inside it const container = this.markup('div', [this.canvas, this.clearButton], { className: 'signature-container' }) - + + if (this.userData) { + this.loadSignature(this.userData) + } + return [this.labelSpan, container] - } /** @@ -47,11 +52,10 @@ export default class controlSignaturePad extends control { */ clearCanvas() { const context = this.canvas.getContext('2d') - - context.beginPath() // Start a new path to clear previous strokes - - context.clearRect(0, 0, this.canvas.width, this.canvas.height) - + if (context) { + context.beginPath() + context.clearRect(0, 0, this.canvas.width, this.canvas.height) + } } /** @@ -60,51 +64,82 @@ export default class controlSignaturePad extends control { */ onRender(evt) { this.canvas.width = this.canvas.parentElement.offsetWidth - this.canvas.height = 150 // Fixed height for the canvas + this.canvas.height = 150 const context = this.canvas.getContext('2d') - context.strokeStyle = '#000' - context.lineWidth = 2 + if (context) { + context.strokeStyle = '#000' + context.lineWidth = 2 - let isDrawing = false - this.canvas.addEventListener('mousedown', e => { - isDrawing = true - context.moveTo(e.offsetX, e.offsetY) - }) - this.canvas.addEventListener('mousemove', e => { - if (isDrawing) { - context.lineTo(e.offsetX, e.offsetY) - context.stroke() - } - }) - this.canvas.addEventListener('mouseup', () => { - isDrawing = false - }) - this.canvas.addEventListener('mouseout', () => { - isDrawing = false - }) + let isDrawing = false + this.canvas.addEventListener('mousedown', e => { + isDrawing = true + context.moveTo(e.offsetX, e.offsetY) + }) + this.canvas.addEventListener('mousemove', e => { + if (isDrawing) { + context.lineTo(e.offsetX, e.offsetY) + context.stroke() + } + }) + this.canvas.addEventListener('mouseup', () => { + isDrawing = false + this.saveSignature() + }) + this.canvas.addEventListener('mouseout', () => { + isDrawing = false + }) + + // Load saved user data if available + if (this.userData) { + this.loadSignature(this.userData) + } + } return evt } - /** - * extend the default events to add a prerender for the signature pad - * @param {string} eventType - * @return {Function} prerender function - */ - on(eventType) { - if (eventType === 'prerender' && this.preview) { - return element => { - if (this.field) { - element = this.field - } - // if this is a preview, stop events bubbling up so the editor preview is clickable (and not draggable) - $(element).on('mousedown', e => { - e.stopPropagation() - }) + /** + * Load signature data from userData + * @param {Array} userData + */ + loadSignature(userData) { + const context = this.canvas.getContext('2d') + const image = new Image() + image.onload = () => { + context.drawImage(image, 0, 0) + } + image.src = JSON.parse(userData[0]) + } + + /** + * Save the signature data to user data + */ + saveSignature() { + const dataUrl = this.canvas.toDataURL('image/png') + this.config.userData = [dataUrl] + console.log('save signature:', this.config.userData) + } + /** + * extend the default events to add a prerender for the signature pad + * @param {string} eventType + * @return {Function} prerender function + */ + on(eventType) { + if (eventType === 'prerender' && this.preview) { + return element => { + if (this.field) { + element = this.field } + + $(element).on('mousedown', e => { + e.stopPropagation() + }) } - return super.on(eventType) } + return super.on(eventType) + } + } + control.register('signaturePad', controlSignaturePad) \ No newline at end of file diff --git a/tests/control/signaturePad.test.js b/tests/control/signaturePad.test.js new file mode 100644 index 000000000..9326746c8 --- /dev/null +++ b/tests/control/signaturePad.test.js @@ -0,0 +1,100 @@ +import controlSignaturePad from '../../src/js/control/signaturePad.js'; + +describe('controlSignaturePad', () => { + let controlInstance; + + beforeEach(() => { + // Mocking the necessary config and functions + const config = { label: 'Signature', userData: null }; + controlInstance = new controlSignaturePad(config); + document.body.innerHTML = '
'; + const builtElements = controlInstance.build(); + document.getElementById('root').appendChild(builtElements[1]); + controlInstance.onRender({}); + }); + + test('should create a canvas and clear button', () => { + const canvas = document.querySelector('canvas.signature-pad'); + const clearButton = document.querySelector('button.clear-button'); + + expect(canvas).not.toBeNull(); + expect(clearButton).not.toBeNull(); + expect(clearButton.textContent).toBe('Clear Signature'); + }); + + test('should clear the canvas when clear button is clicked', () => { + const canvas = document.querySelector('canvas.signature-pad'); + expect(canvas).not.toBeNull(); + + // Set the canvas dimensions + canvas.width = 200; + canvas.height = 150; + + const context = canvas.getContext('2d'); + expect(context).not.toBeNull(); + + // Draw something on the canvas + context.fillRect(0, 0, canvas.width, canvas.height); + expect(context.getImageData(0, 0, 1, 1).data[3]).toBe(255); // Check if pixel is not empty + + // Click the clear button + const clearButton = document.querySelector('button.clear-button'); + clearButton.click(); + + // Check if the canvas is cleared + expect(context.getImageData(0, 0, 1, 1).data[3]).toBe(0); // Check if pixel is empty + }); + + test('should save signature data on mouseup', () => { + const canvas = document.querySelector('canvas.signature-pad'); + expect(canvas).not.toBeNull(); + + // Set the canvas dimensions + canvas.width = 200; + canvas.height = 150; + + const context = canvas.getContext('2d'); + expect(context).not.toBeNull(); + + // Simulate drawing on the canvas + const mouseDownEvent = new MouseEvent('mousedown', { offsetX: 10, offsetY: 10 }); + const mouseMoveEvent = new MouseEvent('mousemove', { offsetX: 20, offsetY: 20 }); + const mouseUpEvent = new MouseEvent('mouseup'); + + canvas.dispatchEvent(mouseDownEvent); + canvas.dispatchEvent(mouseMoveEvent); + canvas.dispatchEvent(mouseUpEvent); + + // Check if saveSignature was called and userData is updated + expect(controlInstance.config.userData).not.toBeNull(); + expect(controlInstance.config.userData[0]).toContain('data:image/png;base64,'); + }); + + test('should load saved signature data on render', () => { + const canvas = document.querySelector('canvas.signature-pad'); + expect(canvas).not.toBeNull(); + + // Set the canvas dimensions + canvas.width = 200; + canvas.height = 150; + + const context = canvas.getContext('2d'); + expect(context).not.toBeNull(); + + // Mock user data + const userData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA'; + controlInstance.config.userData = [userData]; + + // Simulate rendering + controlInstance.onRender({}); + + // Check if the canvas is updated with the saved data + const image = new Image(); + image.src = userData; + image.onload = () => { + context.drawImage(image, 0, 0); + expect(context.getImageData(0, 0, 1, 1).data[3]).toBeGreaterThan(0); // Check if pixel is not empty + }; + }); + +}); \ No newline at end of file