Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a new signature pad element and styling for the new element #1576

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/js/control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,4 +21,5 @@ export default {
controlTextarea,
controlTinymce,
controlQuill,
controlSignaturePad,
}
145 changes: 145 additions & 0 deletions src/js/control/signaturePad.js
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions src/sass/_controls.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✂️ these comment, looks ai generated.

right: 10px; /* Adjust as needed */
right: 10px;
background-color: #f8f8f8;
border: 1px solid #ccc;
padding: 5px;
cursor: pointer;
}
24 changes: 24 additions & 0 deletions src/sass/form-render.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks repeated from above. maybe signature style can be its own file that is imported where needed.

right: 10px; /* Adjust as needed */
right: 10px;
background-color: #f8f8f8;
border: 1px solid #ccc;
padding: 5px;
cursor: pointer;
}
}
100 changes: 100 additions & 0 deletions tests/control/signaturePad.test.js
Original file line number Diff line number Diff line change
@@ -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 = '<div id="root"></div>';
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
};
});

});