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..d5a981136 --- /dev/null +++ b/src/js/control/signaturePad.js @@ -0,0 +1,145 @@ +import control from '../control' + +/** + * SignaturePad class + * @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: '🖊️', + 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 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) + this.clearCanvas() + }) + + this.labelSpan = this.markup('span', this.config.label, { className: 'form-label' }) + + const container = this.markup('div', [this.canvas, this.clearButton], { className: 'signature-container' }) + + if (this.userData) { + this.loadSignature(this.userData) + } + + return [this.labelSpan, container] + } + + /** + * Clear the canvas and any existing drawing data + */ + clearCanvas() { + const context = this.canvas.getContext('2d') + if (context) { + context.beginPath() + 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 + + const context = this.canvas.getContext('2d') + 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.saveSignature() + }) + this.canvas.addEventListener('mouseout', () => { + isDrawing = false + }) + + // Load saved user data if available + if (this.userData) { + this.loadSignature(this.userData) + } + } + return evt + } + + + /** + * 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) + } + +} + +control.register('signaturePad', controlSignaturePad) \ No newline at end of file diff --git a/src/sass/_controls.scss b/src/sass/_controls.scss index cc8b417e2..6b5caa6aa 100644 --- a/src/sass/_controls.scss +++ b/src/sass/_controls.scss @@ -178,3 +178,26 @@ } } } +.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 diff --git a/src/sass/form-render.scss b/src/sass/form-render.scss index 1c6ea2d2a..3780c35f8 100644 --- a/src/sass/form-render.scss +++ b/src/sass/form-render.scss @@ -46,4 +46,28 @@ height: auto; } } +.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 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