/** * @class Ext.signature.Signature * @extend Ext.signature.Base * @alias widget.signature * A lightweight and configurable signature pad component for * capturing user-drawn signatures using canvas. * * This component is built on top of the SignaturePad JavaScript * library and provides integration with * ExtJS layout and configurable pen settings. * * @since 8.0.0 * * ## Example * * ```javascript * @example({ framework: 'extjs' }) * Ext.application({ * name: 'SimpleSignatureApp', * launch: function() { * Ext.create('Ext.Panel', { * renderTo: Ext.getBody(), * width: '100%', * height: '100%', * layout: 'fit', * items: [{ * xtype: 'signature', * penColor: '#000', * penStrokeWidth: 2, * minStrokeRatio: 0.7, * listeners: { * beginStroke: function(cmp) { * console.log('Signature beginStroke:', cmp); * }, * endStroke: function(cmp) { * console.log('Signature endStroke:', cmp); * } * } * }] * }); * } * }); * ``` */ Ext.define('Ext.signature.Signature', { extend: 'Ext.signature.Base', alias: 'widget.signature', config: { /** * @cfg {String} penColor * The color of the pen stroke used to draw on the canvas. * * @since 8.0.0 */ penColor: '#000000', /** * @cfg {String} backgroundColor * The background color of the signature canvas. * * Accepts any valid CSS color value. This is rendered beneath the signature strokes. * * @since 8.0.0 */ backgroundColor: '#ffffff', /** * @cfg {Number} penStrokeWidth * A general stroke width value used to set both min and max stroke width. * * If `penStrokeMinWidth` or `penStrokeMaxWidth` are not specified, this config is used * to compute their values dynamically. * * Default: `2` * * @since 8.0.0 */ penStrokeWidth: 2, /** * @cfg {Number} penStrokeMinWidth * Minimum stroke width in pixels. * * Used by the internal SignaturePad algorithm to determine the thinnest part of a stroke. * * Default: `0.5` * * @since 8.0.0 */ minStrokeRatio: 0.5, /** * @cfg {Number} throttle * The minimum time interval (in milliseconds) between canvas updates while drawing. * * Higher values can improve performance but may reduce stroke fidelity. * * Default: `16` * * @since 8.0.0 */ throttle: 16, /** * @cfg {Number} minDistance * The minimum distance (in pixels) between points in a stroke. * * Smaller values result in smoother curves but require more computation. * * Default: `5` * * @since 8.0.0 */ minDistance: 5, /** * @cfg {String} emptyText * The fallback text to display when the SignaturePad library is not loaded or available. * * Default: `'SignaturePad library not loaded'` * * @since 8.0.0 */ emptyText: 'SignaturePad library not loaded' }, /** * @private * @readonly * Buffer to store undone strokes for redo functionality. */ redoBuffer: [], /** * @private * @readonly * The default filename used when downloading the signature. */ defaultFileName: 'signature', /** * @private * @readonly * The default image format used when downloading the signature. * Valid values: 'png', 'jpeg' */ defaultFormat: 'png', /** * Initializes the SignaturePad instance with configuration. */ initSignaturePad: function() { var me = this, canvas = me.canvas.dom, options = { penColor: me.getPenColor(), backgroundColor: me.getBackgroundColor(), minWidth: me.getPenStrokeWidth() * me.getMinStrokeRatio(), maxWidth: me.getPenStrokeWidth(), throttle: me.getThrottle(), minDistance: me.getMinDistance() }, signaturePad = new window.SignaturePad(canvas, options); me.signaturePad = signaturePad; signaturePad.addEventListener("beginStroke", me.onBegingStroke.bind(me)); signaturePad.addEventListener("endStroke", me.onEndStroke.bind(me)); }, resizeCanvas: function() { var me = this, canvas = me.canvas, canvasEl, signaturePad, data, width, height; if (!canvas) { return; } canvasEl = canvas.dom; signaturePad = me.getSignaturePad(); if (signaturePad) { data = signaturePad.toData(); width = me.el.getWidth(); height = me.el.getHeight(); canvasEl.width = width; canvasEl.height = height; canvas.setStyle({ width: width + 'px', height: height + 'px' }); signaturePad.clear(); signaturePad.fromData(data); } }, /** * @event beginStroke * Fires when the user begins drawing a stroke on the signature pad. * Typically triggered on pointer down or pen contact. * * @param {Ext.signature.Signature} this The signature component instance. * @param {Ext.event.Event } e The original pointer or mouse event. */ /** * @event endStroke * Fires when the user finishes drawing a stroke on the signature pad. * Typically triggered on pointer up or pen lift. * * @param {Ext.signature.Signature} this The signature component instance. * @param {Ext.event.Event } e The original pointer or mouse event. */ /** * Returns the internal SignaturePad instance. * * This is intended for internal or advanced use. * Avoid calling methods directly on it unless absolutely necessary. */ getSignaturePad: function() { return this.signaturePad; }, onBegingStroke: function(e) { this.redoBuffer.length = 0; this.fireEvent('beginStroke', this, e); }, onEndStroke: function(e) { this.fireEvent('endStroke', this, e); }, /** * Clears the canvas content. */ clear: function() { var signaturePad = this.getSignaturePad(); if (signaturePad) { signaturePad.clear(); } }, /** * Checks if the canvas is empty. * @return {Boolean} */ isEmpty: function() { var signaturePad = this.getSignaturePad(); return !signaturePad || signaturePad.isEmpty(); }, /** * Returns the signature as a base64 data URL. * @param {String} type MIME type (e.g. 'image/png') * @param {Number} encoderOptions Number between 0 and 1 representing image quality for * image/jpeg or image/webp * @return {String|null} */ getToDataURL: function(type, encoderOptions) { var signaturePad = this.getSignaturePad(); return signaturePad ? signaturePad.toDataURL(type, encoderOptions) : null; }, /** * Returns the internal stroke data. * @return {Array} */ getToData: function() { var signaturePad = this.getSignaturePad(); return signaturePad ? signaturePad.toData() : []; }, /** * Loads signature data from an array of stroke points. * @param {Array} pointGroups */ fromData: function(pointGroups) { var signaturePad = this.getSignaturePad(); if (signaturePad) { signaturePad.fromData(pointGroups); } }, /** * Loads a signature image from a base64 Data URL. * * @param {String} dataUrl The data URL (e.g. "data:image/png;base64,...") * @param {Object} [options] Optional config for how the image is drawn: * - {Number} ratio Scaling factor for the image (default: 1). * - {Boolean} width Crop width to canvas width (default: true). * - {Boolean} height Crop height to canvas height (default: true). * - {Boolean} x, y Position offsets. */ fromDataURL: function(dataUrl, options) { var signaturePad = this.getSignaturePad(); if (signaturePad && Ext.isString(dataUrl)) { try { signaturePad.fromDataURL(dataUrl, options || {}); } catch (e) { Ext.Logger.warn('Invalid data URL for SignaturePad'); } } }, /** * Triggers download of the signature image. * @param {String} filename Name without extension * @param {String} format 'png', 'jpeg', or 'webp' */ downloadSignature: function(filename, format) { var me = this, dataURL, link, fmt, width, height, svg, bgColor, rawSVG; if (me.isEmpty()) { Ext.Logger.warn('No Signature'); return; } filename = filename || me.defaultFileName; format = format || me.defaultFormat; fmt = format.toLowerCase(); if (fmt === 'svg') { // Get plain SVG markup rawSVG = me.getSignaturePad().toSVG(); // Get canvas dimensions width = me.signaturePad.canvas.width; height = me.signaturePad.canvas.height; // Pick background color from config (fallback = white) bgColor = me.getBackgroundColor ? me.getBackgroundColor() : (me.backgroundColor || 'white'); // Remove any existing width/height/viewBox svg = rawSVG .replace(/\swidth="[^"]*"/, '') .replace(/\sheight="[^"]*"/, '') .replace(/\sviewBox="[^"]*"/, ''); // Add width, height, viewBox svg = svg.replace( /<svg([^>]*)>/, '<svg$1 width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + ' ' + height + '">' ); // Insert background rect with dynamic color svg = svg.replace( /(<svg[^>]*>)/, '$1<rect width="' + width + '" height="' + height + '" fill="' + bgColor + '"/>' ); // Encode SVG to data URL dataURL = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); } else { dataURL = me.getToDataURL('image/' + fmt); } if (dataURL) { link = document.createElement('a'); link.download = filename + '.' + format; link.href = dataURL; link.click(); link = null; } }, /** * @private * Ensures the color is a valid CSS color. * Adds '#' to hex codes if missing. * Returns normalized color if valid, else null. */ normalizeColor: function(color) { if (Ext.isString(color) && /^[0-9A-Fa-f]{3,6}$/.test(color)) { return '#' + color; } return color; }, /** * Updates the pen color on the SignaturePad instance. */ updatePenColor: function(color) { var signaturePad = this.getSignaturePad(), data; if (signaturePad) { data = signaturePad.toData(); signaturePad.penColor = color; signaturePad.clear(); signaturePad.fromData(data); } }, applyPenColor: function(color) { return this.normalizeColor(color); }, /** * Updates the background color and clears the pad. */ updateBackgroundColor: function(color) { var signaturePad = this.getSignaturePad(), data; if (signaturePad) { data = signaturePad.toData(); signaturePad.backgroundColor = color; signaturePad.clear(); signaturePad.fromData(data); } }, applyBackgroundColor: function(color) { return this.normalizeColor(color); }, /** * Undoes the last drawn stroke. */ undo: function() { var me = this, signaturePad = me.getSignaturePad(), data; if (signaturePad) { data = signaturePad.toData(); if (data.length) { me.redoBuffer.push(data.pop()); signaturePad.fromData(data); } } }, /** * Redoes the last undone stroke. */ redo: function() { var me = this, signaturePad = me.getSignaturePad(), redoData, currentData; if (signaturePad && me.redoBuffer.length) { redoData = me.redoBuffer.pop(); currentData = signaturePad.toData(); currentData.push(redoData); signaturePad.fromData(currentData); } }, /** * Updates both min and max width based on penStrokeWidth config. */ updatePenStrokeWidth: function(width) { var signaturePad = this.getSignaturePad(); if (signaturePad && Ext.isNumber(width)) { signaturePad.maxWidth = width; // Ensure minimum stroke width scales with canvas size signaturePad.minWidth = Math.max(1, width * this.getMinStrokeRatio()); signaturePad.penStrokeWidth = width; } }, /** * Cleans up SignaturePad instance and event listeners. */ destroy: function() { var me = this, signaturePad = me.getSignaturePad(); if (signaturePad) { me.signaturePad = null; } me.un('resize', me.resizeCanvas, me); me.redoBuffer = null; me.callParent(); Ext.destroyMembers(me, 'canvas'); }});