jquery-ui-pinpad
Version:
An interactive Pinpad widget for point of sale (POS) or encrypted pinpad (EPP) simulation
952 lines (861 loc) • 33.4 kB
JavaScript
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [
"jquery",
"jquery-ui"
], factory );
} else {
// Browser globals
factory( jQuery );
}
}( function( $ ) {
"use strict";
$.extend( $.ui.keyCode, {
NUMPAD_0: 96,
NUMPAD_1: 97,
NUMPAD_2: 98,
NUMPAD_3: 99,
NUMPAD_4: 100,
NUMPAD_5: 101,
NUMPAD_6: 102,
NUMPAD_7: 103,
NUMPAD_8: 104,
NUMPAD_9: 105,
DECIMAL_POINT: 110
} );
$.widget( "ui.pinpad", {
/**
* The semantic version number of the released jQuery UI Framework.
*/
version: "1.6.1",
/**
* The HTML element on which the pinpad widget should be bound.
*/
defaultElement: "<input>",
/**
* The options hash of the pinpad widget.
*/
options: {
/**
* Specify the container of the pinpad widget. Must be a valid HTML element or jQuery
* selector. If no matching DOM element is found during rendering, the widget is
* inserted directly after the pinpad input.
*/
appendTo: null,
/**
* Specify whether the confirm event is triggered when the pinpad input value length
* reach its max length.
*/
autoComplete: false,
/**
* Specify additional classes to add to the widget element.
*/
classes: {
"ui-pinpad": "ui-corner-all"
},
/**
* Specify whether the correct command of the pinpad widget must clear the input
* content. If set to true, the correct command clears the pin pad input content,
* otherwise only the last inserted character is removed.
*/
clear: false,
/**
* Defines the command buttons of this pin pad.
*/
commands: [
{
position: 0,
name: "cancel",
options: { icon: "ui-icon-close", iconPosition: "end" }
},
{
position: 1,
name: "correct",
options: { icon: "ui-icon-caret-1-w", iconPosition: "end" }
},
{
position: 2,
name: "confirm",
options: { icon: "ui-icon-radio-off", iconPosition: "end" }
}
],
/**
* Specify whether the pin pad input accept only digits. If set to true, the pinpad
* input should not accept the decimal point.
*/
digitOnly: false,
/**
* Specify settings for localization.
*/
display: {
decPoint: ".",
cancel: "Cancel",
correct: "Correct",
confirm: "Confirm"
},
/**
* If and how to animate the hiding of the pinpad widget.
*/
hide: true,
/**
* Defines the pin pad keys layout.
*/
keys: [
"1 2 3",
"4 5 6",
"7 8 9",
"{empty} 0 {dec}"
],
/**
* Specify the minimum number of characters allowed to enable the confirm command.
*/
minLength: 0,
/**
* Specify the maximum number of characters allowed in the pin pad input.
*/
maxLength: Number.POSITIVE_INFINITY,
/**
* If and how to animate the showing of the pinpad widget.
*/
show: true,
/**
* Specify the last confirmed value of the pinpad.
*/
value: "",
// Callbacks
cancel: null,
change: null,
close: null,
confirm: null,
keypress: null,
open: null
},
/**
* Creates the pinpad widget.
* @private
*/
_create: function() {
var inst = this;
var output = $( inst.options.output );
var maxLength = inst.element.prop( "maxLength" );
var container = inst._appendTo();
inst.element.uniqueId();
if ( output.length === 0 ) {
output = inst.element.clone()
.attr( "id", inst.element.attr( "id" ) + "_output" )
.removeAttr( "maxlength" );
output.insertAfter( inst.element );
}
output
.addClass( "ui-pinpad-output" )
.attr( "role", "pinpad-output" );
$( "label[for='" + inst.element.attr( "id" ) + "']" ).each( function( index, element ) {
$( element ).attr( "for", output.attr( "id" ) );
} );
inst.outputElement = output;
inst.element.val( inst.options.value );
inst.element.attr( "role", "pinpad-input" );
inst._addClass( "ui-pinpad-input", "ui-helper-hidden" );
if ( inst.element.is( ":disabled" ) ) {
inst.options.disabled = true;
}
if ( maxLength > 0 ) {
maxLength = inst.options.maxLength ?
Math.min( inst.options.maxLength, maxLength ) :
maxLength;
inst.options.maxLength = Math.max( inst.options.minLength, maxLength );
}
if ( !inst.options.formatter ) {
inst.options.formatter = $.ui.pinpad.defaultFormatter;
}
inst.ppDiv = $( "<div></div>" )
.appendTo( container.length ? container : document.body )
.data( "input", inst.element );
inst._addClass( inst.ppDiv, "ui-pinpad", "ui-widget ui-widget-content" );
inst._bindInputEvents();
inst.refresh();
if ( !container.length ) {
delete inst.options.appendTo;
inst._on( document.body, {
"pinpadcancel": function( event ) {
inst._close( event.originalEvent );
},
"pinpadconfirm": function( event ) {
inst._close( event.originalEvent );
}
} );
inst._on( inst.outputElement, {
"focusin": function( event ) {
inst.open( event );
},
"focusout": function( event ) {
if ( event.relatedTarget != null ) {
var input = $( event.relatedTarget )
.closest( inst.ppDiv ).data( "input" );
if ( !( input && input.attr( "id" ) === inst.element.attr( "id" ) ) ) {
inst.cancel();
}
}
}
} );
inst._addClass( inst.ppDiv, null, "ui-front" );
inst._hide( inst.ppDiv, false );
inst.ppDiv.detach();
} else {
inst._addClass( inst.ppDiv, "ui-pinpad-inline" );
}
},
/**
* Adjusts the pinpad widget position on the screen.
* @private
*/
_adjustPosition: function() {
var div = $( "<div></div>" )
.css( "position", "absolute" )
.appendTo( document.body )
.position( {
my: "left top",
at: "left bottom",
of: this.outputElement
} );
this.ppDiv.css( div.css( [ "left", "top" ] ) );
div.remove();
},
/**
* Returns the jQuery object which contains the HTML element that will contains the pinpad.
* If the jQuery object length is 0, then the pinpad widget will be appended to the
* document body element.
* @returns {jQuery} the pinpad container wrapper.
* @private
*/
_appendTo: function() {
var container = $();
if ( this.options.appendTo ) {
container = container.add( this.options.appendTo );
}
return container;
},
/**
* Checks if the decimal point key should be enabled.
* @private
*/
_checkDecimalKey: function() {
var value = this.element.val();
this.ppDiv.find( ".ui-pinpad-key-num-pad-dec" )
.button( "option", "disabled", this.options.digitOnly || /\./.test( value ) );
},
/**
* Checks and adjusts the state of the confirm command button.
* @private
*/
_checkConfirmCommand: function() {
var value = this.element.val();
if ( this.element.is( ":enabled" ) ) {
var confirmCommand = this.ppDiv.find( ".ui-pinpad-command-confirm" );
var confirmDisabled = confirmCommand.button( "option", "disabled" );
var invalid = ( value.length < this.options.minLength ) ||
( this.options.autoComplete &&
( value.length < this.options.maxLength ) );
invalid = invalid || this.element.is( ".ui-pinpad-input-invalid" );
if ( invalid && !confirmDisabled ) {
confirmCommand.button( "disable" );
} else if ( !invalid && confirmDisabled ) {
confirmCommand.button( "enable" );
}
if ( !invalid &&
this.options.autoComplete &&
value.length === this.options.maxLength ) {
this.confirm();
}
}
},
/**
* Renders the pinpad numeric key buttons.
* @private
*/
_drawKeys: function() {
var inst = this;
var keys = [];
var numPad = $( "<div class='ui-pinpad-num-pad'></div>" ).appendTo( this.ppDiv );
var table = $( "<table></table>" ).appendTo( numPad );
var firstKeyCode = $.ui.keyCode.NUMPAD_0;
var row;
var col;
var i, j;
$.each( this.options.keys, function( index, row ) {
keys.push( row.split( " " ) );
} );
for ( i = 0; i < keys.length; i++ ) {
row = $( "<tr></tr>" ).appendTo( table );
for ( j = 0; j < keys[ i ].length; j++ ) {
var keyId = keys[ i ][ j ];
var digit = parseInt( keyId, 10 );
col = $( "<td></td>" ).appendTo( row );
if ( !isNaN( digit ) ) {
$( "<button type='button' class='ui-pinpad-key ui-pinpad-key-num-pad-" +
digit + "' data-key-code='" + ( firstKeyCode + digit ) + "' value='" +
digit + "'>" + digit + "</button>" ).appendTo( col ).button();
} else {
if ( /^\{\S+\}$/.test( keyId ) ) {
keyId = keyId.match( /^\{(\S+)\}$/ )[ 1 ];
if ( keyId === "dec" ) {
$( "<button type='button' " +
"class='ui-pinpad-key ui-pinpad-key-num-pad-dec' " +
"data-key-code='" + $.ui.keyCode.DECIMAL_POINT +
"' value='.'>" + this.options.display.decPoint + "</button>" )
.appendTo( col ).button( { disabled: this.options.digitOnly } );
} else if ( keyId === "empty" ) {
$( "<button type='button' " +
"class='ui-pinpad-key ui-pinpad-key-num-pad-empty' " +
"data-key-code='0'> </button>" )
.appendTo( col ).button();
}
}
}
}
}
this.ppDiv.find( ".ui-pinpad-key" ).each( function( index, element ) {
var button = $( element );
button.attr( "tabindex", "0" );
inst._bindKeyEvents( button );
} );
},
/**
* Renders the command panel buttons.
* @private
*/
_drawCommands: function() {
var that = this;
var commands = that.options.commands;
var commandPanel = $( "<div class='ui-pinpad-command-panel'></div>" )
.appendTo( this.ppDiv );
var button;
var i;
var count = 4;
var table = $( "<table></table>" ).appendTo( commandPanel );
for ( i = 0; i < count; i++ ) {
var row = $( "<tr></tr>" ).appendTo( table );
var col = $( "<td></td>" ).appendTo( row );
button = $( "<button class='ui-pinpad-command' type='button'> </button>" )
.appendTo( col ).button();
}
$.each( commands, function( index, command ) {
button = commandPanel.find( "tr:eq(" + command.position + ") .ui-pinpad-command" )
.addClass( "ui-pinpad-command-" + command.name )
.attr( "name", command.name )
.attr( "tabindex", "0" )
.button( "option", $.extend( {}, command.options, {
label: that.options.display[ command.name ]
} ) );
that._bindCommandEvents( button );
} );
},
/**
* Updates the command button labels using the provided display object.
* @param commands
* @param display
* @private
*/
_updateCommands: function( commands, display ) {
var commandPanel = this.ppDiv.find( ".ui-pinpad-command-panel" );
$.each( commands, function( index, command ) {
commandPanel.find( ".ui-pinpad-command-" + command.name )
.button( "option", {
label: display[ command.name ]
} );
} );
},
/**
* Bind events on the pinpad input and output elements.
* @private
*/
_bindInputEvents: function() {
this._on( this.element, {
change: function( event ) {
this._trigger( "change", event );
this._displayValue();
this._checkDecimalKey();
this._checkConfirmCommand();
event.preventDefault();
}
} );
this._on( this.outputElement, {
focus: function( event ) {
var element = event.target;
if ( element.setSelectionRange ) {
var len = element.value.length;
element.setSelectionRange( len, len );
} else {
var $el = $( element );
$el.val( $el.val() );
}
},
keydown: function( event ) {
var keyCode = event.keyCode;
switch ( keyCode ) {
case $.ui.keyCode.HOME:
case $.ui.keyCode.END:
case $.ui.keyCode.LEFT:
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
case $.ui.keyCode.TAB:
return;
case $.ui.keyCode.BACKSPACE:
if ( !this.options.clear ) {
this.ppDiv.find( ".ui-pinpad-command-correct" ).click();
}
break;
case $.ui.keyCode.DELETE:
if ( this.options.clear ) {
this.ppDiv.find( ".ui-pinpad-command-correct" ).click();
}
break;
case $.ui.keyCode.ENTER:
this.ppDiv.find( ".ui-pinpad-command-confirm" ).click();
break;
case $.ui.keyCode.ESCAPE:
this.ppDiv.find( ".ui-pinpad-command-cancel" ).click();
break;
case $.ui.keyCode.DECIMAL_POINT:
this.ppDiv.find( ".ui-pinpad-key-num-pad-dec" ).click();
break;
default:
if ( $.ui.pinpad.isDigit( keyCode ) ) {
this.ppDiv.find( ".ui-pinpad-key-num-pad-" +
( keyCode - $.ui.keyCode.NUMPAD_0 ) ).click();
}
}
event.preventDefault();
}
} );
},
/**
* Bind events on pinpad key button element.
* @param button
* @private
*/
_bindKeyEvents: function( button ) {
this._on( button, {
click: function( event ) {
var keyCode = button.data( "keyCode" );
var value = this.element.val();
if ( ( $.ui.pinpad.isDigit( keyCode ) ||
( $.ui.pinpad.isDecimalPoint( keyCode ) &&
value.indexOf( "." ) === -1 ) ) &&
value.length < this.options.maxLength &&
this._trigger( "keypress", event, { keyCode: keyCode } ) ) {
this._insertText( button.val() );
}
}
} );
},
/**
* Bind events on pinpad command button element.
* @param button
* @private
*/
_bindCommandEvents: function( button ) {
this._on( button, {
click: function( event ) {
if ( button.is( ".ui-pinpad-command-correct" ) ) {
if ( this.options.clear ) {
this._clear();
} else {
this._backspace();
}
} else {
this._processCommand( button, event );
}
}
} );
},
/**
* Get the pinpad widget buttons that correspond to the given key code.
* @param keyCode the key code to match
* @returns {*|{}} a jQuery object with the set of button elements.
* @private
*/
_findButtons: function( keyCode ) {
var buttons = $();
switch ( keyCode ) {
case $.ui.keyCode.BACKSPACE:
if ( !this.options.clear ) {
buttons = this.ppDiv.find( ".ui-pinpad-command-correct" );
}
break;
case $.ui.keyCode.DELETE:
if ( this.options.clear ) {
buttons = this.ppDiv.find( ".ui-pinpad-command-correct" );
}
break;
case $.ui.keyCode.ENTER:
buttons = this.ppDiv.find( ".ui-pinpad-command-confirm" );
break;
case $.ui.keyCode.ESCAPE:
buttons = this.ppDiv.find( ".ui-pinpad-command-cancel" );
break;
case $.ui.keyCode.DECIMAL_POINT:
buttons = this.ppDiv.find( ".ui-pinpad-key-num-pad-dec" );
break;
default:
if ( $.ui.pinpad.isDigit( keyCode ) ) {
buttons = this.ppDiv.find( ".ui-pinpad-key-num-pad-" +
( keyCode - $.ui.keyCode.NUMPAD_0 ) );
}
}
return buttons;
},
/**
* Formats the pinpad input value and display the result on the output element.
* @private
*/
_displayValue: function() {
var value = this.element.val();
var options = this.options;
var formattedValue = this.options.formatter.format( value, options);
this.outputElement.val( formattedValue );
},
/**
* Insert the given value of the pressed button.
* @param value the value to insert.
* @private
*/
_insertText: function( value ) {
var currentValue = this.element.val();
this.outputElement.each( function( index, element ) {
if ( element.createTextRange ) {
var range = element.createTextRange();
range.collapse( false );
range.select();
} else {
element.blur();
element.selectionStart = element.selectionEnd = element.value.length;
element.focus();
}
} );
if ( value == "\b" ) {
this.value( currentValue.substring( 0, currentValue.length - 1 ) );
} else {
this.value( currentValue + value );
}
},
/**
* Check if the given value can be used as pin pad raw value.
* @param value the value to check.
* @returns {boolean} true if the value is valid, otherwise false.
* @private
*/
_isValid: function( value ) {
var valid = false;
if ( typeof value === "string" ) {
valid = ( value.length === 0 ) ||
( $.ui.pinpad.isDecimalNumber( value ) &&
!( this.options.digitOnly && value.indexOf( "." ) > -1 ) );
}
return valid;
},
_processCommand: function( button, event ) {
var name = button.attr( "name" );
var callback = this[ "_" + name ];
if ( !( this._trigger( name, event ) === false ) &&
$.isFunction( callback ) ) {
callback.apply( this );
}
},
_cancel: function() {
this.value( this.option( "value" ) );
},
_confirm: function() {
this.option( "value", this.value() );
},
/**
* Returns an object for extending or overwriting the default pinpad options.
* @returns {Object} the options to use for extension.
* @private
*/
_getCreateOptions: function() {
return {
value: this._getDefaultValue()
};
},
/**
* Returns the options of this pin pad.
* @returns {Object} the options of the pin pad.
* @private
*/
_getCreateEventData: function() {
return { options: this.options };
},
/**
* Returns the default value of this pin pad.
* @returns {string} the default value of the pin pad.
* @private
*/
_getDefaultValue: function() {
var inputVal = this.element.val();
return inputVal && this._isValid( inputVal ) ? inputVal : "";
},
/**
* Get the raw value of this pin pad.
* @returns {string} the raw value of this pinpad.
* @private
*/
_getValue: function() {
return this.element.val();
},
/**
* Set the raw value of this pin pad.
* @param newValue the new value to set.
* @private
*/
_setValue: function( newValue ) {
if ( this._isValid( newValue ) ) {
if ( this.ppDiv.is( ":hidden" ) ) {
this._show( this.ppDiv, this.options.show );
}
this.element.val( newValue ).change();
}
},
_setOption: function( key, value ) {
this._super( key, value );
if ( key === "display" ) {
this.ppDiv.find( ".ui-pinpad-key-num-pad-dec" )
.button( "option", "label", value.decPoint );
this._updateCommands( this.options.commands, value );
}
if ( key === "value" ) {
this._setValue( value );
}
this._checkDecimalKey();
this._checkConfirmCommand();
},
/**
* Update the pinpad to reflect the given disabled state.
* @param value the disabled state value to set.
* @private
*/
_setOptionDisabled: function( value ) {
var element = this.element,
output = this.outputElement,
widget = this.ppDiv;
this._super( value );
this._toggleClass( element, "ui-state-disabled", null, value );
element.prop( "disabled", value );
this._toggleClass( output, "ui-state-disabled", null, value );
output.prop( "disabled", value );
this._toggleClass( widget, "ui-state-disabled", null, value );
widget.find( "button" ).each( function( index, button ) {
$( button ).button( "option", "disabled", value );
} );
if ( value ) {
output.blur();
}
},
/**
* Restore the original state of input.
* @private
*/
_destroy: function() {
this.ppDiv.remove();
this.outputElement.remove();
this.element.removeAttr( "role" ).removeUniqueId();
},
/**
* Show the pinpad overlay.
* @param event the original event which cause the pinpad to be opened.
* @private
*/
_open: function( event ) {
var inst = this;
inst.ppDiv.appendTo( document.body );
inst._adjustPosition();
inst._show( inst.ppDiv, inst.options.show, function() {
inst._trigger( "open", event );
} );
},
/**
* Hide the pinpad overlay.
* @param event the original event which cause the pinpad to be closed.
* @private
*/
_close: function( event ) {
var inst = this;
inst._hide( inst.ppDiv, inst.options.hide, function() {
inst._trigger( "close", event );
inst.outputElement.blur();
inst.ppDiv.detach();
} );
},
/**
* Removes the last inserted character.
* @private
*/
_backspace: function() {
var currentValue = this.element.val();
var length = currentValue.length;
if ( length > 0 ) {
this._insertText( "\b" );
}
},
/**
* Removes all inserted character.
* @private
*/
_clear: function() {
this.value( "" );
},
/**
* Clears the pinpad input content.
*/
clear: function() {
this._clear();
},
/**
* Sends a cancel command to this pinpad.
*/
cancel: function() {
this.ppDiv.find( ".ui-pinpad-command-cancel" ).click();
},
/**
* Sends a confirm command to this pinpad.
*/
confirm: function() {
this.ppDiv.find( ".ui-pinpad-command-confirm" ).click();
},
/**
* Open the pinpad widget when the given is triggered.
* @param event the event which cause the pinpad to be opened.
*/
open: function( event ) {
if ( this.ppDiv.is( ":hidden" ) ) {
this._open( event );
}
},
/**
* Returns the output element where the formatted value is displayed.
* @returns {Object} the jQuery object containing the unique output DOM element of this
* pinpad.
*/
output: function() {
return this.outputElement;
},
/**
* Render the pinpad with its actual state.
*/
refresh: function() {
var element = this.element;
this.ppDiv.find( "button" ).button( "destroy" );
this.ppDiv.empty();
this._drawKeys();
this._drawCommands();
if ( element.is( ":disabled" ) ) {
this.ppDiv.find( "button" ).button( "disable" );
} else {
this._checkConfirmCommand();
}
},
/**
* Get or set the raw value of this pinpad.
* @param value the new raw value to set if not undefined.
* @returns {string} the current raw value of this pinpad if value is undefined.
*/
value: function( value ) {
if ( value === undefined ) {
return this._getValue();
}
var val = value.toString();
this._setValue( val.substring( 0, Math.min( val.length, this.options.maxLength ) ) );
},
widget: function() {
return this.ppDiv;
}
} );
$.extend( $.ui.pinpad, {
defaultFormatter: {
format: function( value, settings ) {
return value.replace( ".", settings.display.decPoint );
}
},
regional: {
en: { display: $.ui.pinpad.prototype.options.display }
},
/**
* Generates a pin pad keys layout in order to have keys set in random position.
* @returns {Array} the pinpad keys' layout.
*/
generateRandomKeys: function() {
var keys = [];
var digits = [];
var items = [];
var i;
for ( i = 0; i < 10; i++ ) {
digits.push( i );
}
for ( i = digits.length; i > 0; i-- ) {
var index = Math.floor( Math.random() * i );
var digit = digits[ index ];
digits.splice( index, 1 );
items.push( digit );
if ( items.length % 3 === 0 ) {
keys.push( items.join( " " ) );
items = [];
}
}
keys.push( items );
keys[ keys.length - 1 ] = "{empty} " + keys[ keys.length - 1 ] + " {empty}";
return keys;
},
/**
* Checks if the given value contains the decimal point.
* @param value the value to check.
* @returns {boolean}
*/
isDecimalNumber: function( value ) {
return ( /^\d+(\.\d*)?$/ ).test( value );
},
/**
* Checks if the given key code represents the decimal point of the numpad.
* @param keyCode the key code to check.
* @returns {boolean} true if the key code represents the decimal point of the numpad,
* otherwise false.
*/
isDecimalPoint: function( keyCode ) {
return keyCode === $.ui.keyCode.DECIMAL_POINT;
},
/**
* Checks if the given key code represents a digit of the numpad.
* @param keyCode the key code to check
* @returns {boolean} true if the key code represents a digit of the numpad, otherwise
* false.
*/
isDigit: function( keyCode ) {
return keyCode >= $.ui.keyCode.NUMPAD_0 && keyCode <= $.ui.keyCode.NUMPAD_9;
}
} );
$( window ).on( {
"mousedown": function( event ) {
var target = $( event.target );
if ( target.is( ".ui-pinpad-output" ) ) {
return;
}
var input = target.closest( ".ui-pinpad" ).data( "input" );
if ( input ) {
if ( target.is( "button" ) ) {
target.addClass( "ui-state-active" );
}
event.preventDefault();
} else {
$( document.body ).children( ".ui-pinpad" ).each( function( index, element ) {
$( element ).data( "input" ).pinpad( "cancel" );
} );
}
},
"mouseup": function() {
$( document ).find( ".ui-pinpad button.ui-state-active" )
.removeClass( "ui-state-active" );
}
} );
return $.ui.pinpad;
} ) );