Commit 8331ff01 by Kartik Visweswaran

Fix #3196: Masked input widget revamped to use new plugin

parent 62fdaeec
......@@ -51,6 +51,7 @@ Yii Framework 2 Change Log
- Enh #3108: Added `yii\debug\Module::enableDebugLogs` to disable logging debug logs by default (qiangxue)
- Enh #3132: `yii\rbac\PhpManager` now supports more compact data file format (qiangxue)
- Enh #3154: Added validation error display for `GridView` filters (ivan-kolmychek)
- Enh #3196: Masked input upgraded to use jquery.inputmask plugin with more features. (kartik-v)
- Enh #3222: Added `useTablePrefix` option to the model generator for Gii (horizons2)
- Enh #3230: Added `yii\filters\AccessControl::user` to support access control with different actors (qiangxue)
- Enh #3232: Added `export()` and `exportAsString()` methods to `yii\helpers\BaseVarDumper` (klimov-paul)
......
/*
Masked Input plugin for jQuery
Copyright (c) 2007-2013 Josh Bush (digitalbush.com)
Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license)
Version: 1.3.1
*/
(function($) {
function getPasteEvent() {
var el = document.createElement('input'),
name = 'onpaste';
el.setAttribute(name, '');
return (typeof el[name] === 'function')?'paste':'input';
}
var pasteEventName = getPasteEvent() + ".mask",
ua = navigator.userAgent,
iPhone = /iphone/i.test(ua),
android=/android/i.test(ua),
caretTimeoutId;
$.mask = {
//Predefined character definitions
definitions: {
'9': "[0-9]",
'a': "[A-Za-z]",
'*': "[A-Za-z0-9]"
},
dataName: "rawMaskFn",
placeholder: '_',
};
$.fn.extend({
//Helper Function for Caret positioning
caret: function(begin, end) {
var range;
if (this.length === 0 || this.is(":hidden")) {
return;
}
if (typeof begin == 'number') {
end = (typeof end === 'number') ? end : begin;
return this.each(function() {
if (this.setSelectionRange) {
this.setSelectionRange(begin, end);
} else if (this.createTextRange) {
range = this.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', begin);
range.select();
}
});
} else {
if (this[0].setSelectionRange) {
begin = this[0].selectionStart;
end = this[0].selectionEnd;
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
begin = 0 - range.duplicate().moveStart('character', -100000);
end = begin + range.text.length;
}
return { begin: begin, end: end };
}
},
unmask: function() {
return this.trigger("unmask");
},
mask: function(mask, settings) {
var input,
defs,
tests,
partialPosition,
firstNonMaskPos,
len;
if (!mask && this.length > 0) {
input = $(this[0]);
return input.data($.mask.dataName)();
}
settings = $.extend({
placeholder: $.mask.placeholder, // Load default placeholder
completed: null
}, settings);
defs = $.mask.definitions;
tests = [];
partialPosition = len = mask.length;
firstNonMaskPos = null;
$.each(mask.split(""), function(i, c) {
if (c == '?') {
len--;
partialPosition = i;
} else if (defs[c]) {
tests.push(new RegExp(defs[c]));
if (firstNonMaskPos === null) {
firstNonMaskPos = tests.length - 1;
}
} else {
tests.push(null);
}
});
return this.trigger("unmask").each(function() {
var input = $(this),
buffer = $.map(
mask.split(""),
function(c, i) {
if (c != '?') {
return defs[c] ? settings.placeholder : c;
}
}),
focusText = input.val();
function seekNext(pos) {
while (++pos < len && !tests[pos]);
return pos;
}
function seekPrev(pos) {
while (--pos >= 0 && !tests[pos]);
return pos;
}
function shiftL(begin,end) {
var i,
j;
if (begin<0) {
return;
}
for (i = begin, j = seekNext(end); i < len; i++) {
if (tests[i]) {
if (j < len && tests[i].test(buffer[j])) {
buffer[i] = buffer[j];
buffer[j] = settings.placeholder;
} else {
break;
}
j = seekNext(j);
}
}
writeBuffer();
input.caret(Math.max(firstNonMaskPos, begin));
}
function shiftR(pos) {
var i,
c,
j,
t;
for (i = pos, c = settings.placeholder; i < len; i++) {
if (tests[i]) {
j = seekNext(i);
t = buffer[i];
buffer[i] = c;
if (j < len && tests[j].test(t)) {
c = t;
} else {
break;
}
}
}
}
function keydownEvent(e) {
var k = e.which,
pos,
begin,
end;
//backspace, delete, and escape get special treatment
if (k === 8 || k === 46 || (iPhone && k === 127)) {
pos = input.caret();
begin = pos.begin;
end = pos.end;
if (end - begin === 0) {
begin=k!==46?seekPrev(begin):(end=seekNext(begin-1));
end=k===46?seekNext(end):end;
}
clearBuffer(begin, end);
shiftL(begin, end - 1);
e.preventDefault();
} else if (k == 27) {//escape
input.val(focusText);
input.caret(0, checkVal());
e.preventDefault();
}
}
function keypressEvent(e) {
var k = e.which,
pos = input.caret(),
p,
c,
next;
if (e.ctrlKey || e.altKey || e.metaKey || k < 32) {//Ignore
return;
} else if (k) {
if (pos.end - pos.begin !== 0){
clearBuffer(pos.begin, pos.end);
shiftL(pos.begin, pos.end-1);
}
p = seekNext(pos.begin - 1);
if (p < len) {
c = String.fromCharCode(k);
if (tests[p].test(c)) {
shiftR(p);
buffer[p] = c;
writeBuffer();
next = seekNext(p);
if(android){
setTimeout($.proxy($.fn.caret,input,next),0);
}else{
input.caret(next);
}
if (settings.completed && next >= len) {
settings.completed.call(input);
}
}
}
e.preventDefault();
}
}
function clearBuffer(start, end) {
var i;
for (i = start; i < end && i < len; i++) {
if (tests[i]) {
buffer[i] = settings.placeholder;
}
}
}
function writeBuffer() { input.val(buffer.join('')); }
function checkVal(allow) {
//try to place characters where they belong
var test = input.val(),
lastMatch = -1,
i,
c;
for (i = 0, pos = 0; i < len; i++) {
if (tests[i]) {
buffer[i] = settings.placeholder;
while (pos++ < test.length) {
c = test.charAt(pos - 1);
if (tests[i].test(c)) {
buffer[i] = c;
lastMatch = i;
break;
}
}
if (pos > test.length) {
break;
}
} else if (buffer[i] === test.charAt(pos) && i !== partialPosition) {
pos++;
lastMatch = i;
}
}
if (allow) {
writeBuffer();
} else if (lastMatch + 1 < partialPosition) {
input.val("");
clearBuffer(0, len);
} else {
writeBuffer();
input.val(input.val().substring(0, lastMatch + 1));
}
return (partialPosition ? i : firstNonMaskPos);
}
input.data($.mask.dataName,function(){
return $.map(buffer, function(c, i) {
return tests[i]&&c!=settings.placeholder ? c : null;
}).join('');
});
if (!input.attr("readonly"))
input
.one("unmask", function() {
input
.unbind(".mask")
.removeData($.mask.dataName);
})
.bind("focus.mask", function() {
clearTimeout(caretTimeoutId);
var pos,
moveCaret;
focusText = input.val();
pos = checkVal();
caretTimeoutId = setTimeout(function(){
writeBuffer();
if (pos == mask.length) {
input.caret(0, pos);
} else {
input.caret(pos);
}
}, 10);
})
.bind("blur.mask", function() {
checkVal();
if (input.val() != focusText)
input.change();
})
.bind("keydown.mask", keydownEvent)
.bind("keypress.mask", keypressEvent)
.bind(pasteEventName, function() {
setTimeout(function() {
var pos=checkVal(true);
input.caret(pos);
if (settings.completed && pos == input.val().length)
settings.completed.call(input);
}, 0);
});
checkVal(); //Perform initial check for existing values
});
}
});
})(jQuery);
......@@ -11,13 +11,13 @@ use yii\base\InvalidConfigException;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\web\JsExpression;
use yii\web\View;
/**
* MaskedInput generates a masked text input.
*
* MaskedInput is similar to [[Html::textInput()]] except that
* an input mask will be used to force users to enter properly formatted data,
* such as phone numbers, social security numbers.
* MaskedInput is similar to [[Html::textInput()]] except that an input mask will be used to force users to enter
* properly formatted data, such as phone numbers, social security numbers.
*
* To use MaskedInput, you must set the [[mask]] property. The following example
* shows how to use MaskedInput to collect phone numbers:
......@@ -29,38 +29,54 @@ use yii\web\JsExpression;
* ]);
* ~~~
*
* The masked text field is implemented based on the [jQuery masked input plugin](http://digitalbush.com/projects/masked-input-plugin).
* The masked text field is implemented based on the
* [jQuery input mask plugin](https://github.com/RobinHerbots/jquery.inputmask).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Kartik Visweswaran <kartikv2@gmail.com>
* @since 2.0
*/
class MaskedInput extends InputWidget
{
const PLUGIN_NAME = 'inputmask';
/**
* @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined:
* @var string|array|JsExpression the input mask (e.g. '99/99/9999' for date input). The following characters
* can be used in the mask and are predefined:
*
* - `a`: represents an alpha character (A-Z, a-z)
* - `9`: represents a numeric character (0-9)
* - `*`: represents an alphanumeric character (A-Z, a-z, 0-9)
* - `?`: anything listed after '?' within the mask is considered optional user input
* - `[` and `]`: anything entered between the square brackets is considered optional user input. This is
* based on the `optionalmarker` setting in `clientOptions`.
*
* Additional characters can be defined by specifying the [[charMap]] property.
* Additional definitions can be set through definitions property.
*/
public $mask;
/**
* @var array the mapping between mask characters and the corresponding patterns.
* For example, `['~' => '[+-]']` specifies that the '~' character expects '+' or '-' input.
* Defaults to null, meaning using the map as described in [[mask]].
* @var array custom mask definitions to use. Should be configured as `$maskSymbol => $settings`, where:
* - `maskSymbol`: string, a character to identify your mask definition
* - `settings`: array, consisiting of the following entries:
* - `validator`: string, a JS regular expression or a JS function.
* - `cardinality`: int, specifies how many characters are represented and validated for the definition.
* - `prevalidator`: array, validate the characters before the definition cardinality is reached.
* - `definitionSymbol`: string, allows shifting values from other definitions, with this `definitionSymbol`.
*/
public $charMap;
public $definitions;
/**
* @var string the character prompting for user input. Defaults to underscore '_'.
* @var array custom aliases to use. Should be configured as `$maskAlias` => $settings`, where:
* - `maskAlias`: string, a text to identify your mask alias definition (e.g. 'phone')
* - `settings`: array settings for the mask symbol, exactly similar to parameters as passed in `clientOptions`
*/
public $placeholder;
public $aliases;
/**
* @var string a JavaScript function callback that will be invoked when user finishes the input.
* @var array the JQuery plugin options for the input mask plugin.
* @see https://github.com/RobinHerbots/jquery.inputmask
*/
public $completed;
public $clientOptions = [];
/**
* @var array the HTML attributes for the input tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
......@@ -68,19 +84,28 @@ class MaskedInput extends InputWidget
public $options = ['class' => 'form-control'];
/**
* @var string the hashed variable to store the pluginOptions
*/
protected $_hashVar;
/**
* Initializes the widget.
*
* @throws InvalidConfigException if the "mask" property is not set.
*/
public function init()
{
parent::init();
if (empty($this->mask)) {
throw new InvalidConfigException('The "mask" property must be set.');
if (empty($this->mask) && empty($this->clientOptions['alias'])) {
throw new InvalidConfigException("Either the 'mask' property or the 'clientOptions[\"alias\"]' property must be set.");
}
}
/**
*
* Runs the widget.
*
* @return string|void
*/
public function run()
{
......@@ -93,41 +118,60 @@ class MaskedInput extends InputWidget
}
/**
* Registers the needed JavaScript.
* Generates a hashed variable to store the plugin `clientOptions`. Helps in reusing the variable for similar
* options passed for other widgets on the same page. The following special data attributes will also be
* setup for the input widget, that can be accessed through javascript:
* - 'data-plugin-options' will store the hashed variable storing the plugin options.
* - 'data-plugin-name' the name of the plugin
*
* @param View $view the view instance
*/
public function registerClientScript()
protected function hashPluginOptions($view)
{
$options = $this->getClientOptions();
$options = empty($options) ? '' : ',' . Json::encode($options);
$js = '';
if (is_array($this->charMap) && !empty($this->charMap)) {
$js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n";
}
$id = $this->options['id'];
$js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});";
$view = $this->getView();
MaskedInputAsset::register($view);
$view->registerJs($js);
$encOptions = empty($this->clientOptions) ? '{}' : Json::encode($this->clientOptions);
$this->_hashVar = self::PLUGIN_NAME . '_' . hash('crc32', $encOptions);
$this->options['data-plugin-name'] = self::PLUGIN_NAME;
$this->options['data-plugin-options'] = $this->_hashVar;
$view->registerJs("var {$this->_hashVar} = {$encOptions};\n", View::POS_HEAD);
}
/**
* @return array the options for the text field
* Initializes client options
*/
protected function getClientOptions()
protected function initClientOptions()
{
$options = [];
if ($this->placeholder !== null) {
$options['placeholder'] = $this->placeholder;
}
if ($this->completed !== null) {
if ($this->completed instanceof JsExpression) {
$options['completed'] = $this->completed;
} else {
$options['completed'] = new JsExpression($this->completed);
$options = $this->clientOptions;
foreach ($options as $key => $value) {
if (in_array($key, ['oncomplete', 'onincomplete', 'oncleared', 'onKeyUp', 'onKeyDown', 'onBeforeMask',
'onBeforePaste', 'onUnMask', 'isComplete', 'determineActiveMasksetIndex']) && !$value instanceof JsExpression
) {
$options[$key] = new JsExpression($value);
}
}
$this->clientOptions = $options;
}
return $options;
/**
* Registers the needed client script and options.
*/
public function registerClientScript()
{
$js = '';
$view = $this->getView();
$this->initClientOptions();
if (!empty($this->mask)) {
$this->clientOptions['mask'] = $this->mask;
}
$this->hashPluginOptions($view);
if (is_array($this->definitions) && !empty($this->definitions)) {
$js .= '$.extend($.' . self::PLUGIN_NAME . '.defaults.definitions, ' . Json::encode($this->definitions) . ");\n";
}
if (is_array($this->aliases) && !empty($this->aliases)) {
$js .= '$.extend($.' . self::PLUGIN_NAME . '.defaults.aliases, ' . Json::encode($this->aliases) . ");\n";
}
$id = $this->options['id'];
$js .= '$("#' . $id . '").' . self::PLUGIN_NAME . "(" . $this->_hashVar . ");\n";
MaskedInputAsset::register($view);
$view->registerJs($js);
}
}
......@@ -10,16 +10,12 @@ namespace yii\widgets;
use yii\web\AssetBundle;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Kartik Visweswaran <kartikv2@gmail.com>
* @since 2.0
*/
class MaskedInputAsset extends AssetBundle
{
public $sourcePath = '@yii/assets';
public $js = [
'jquery.maskedinput.js',
];
public $depends = [
'yii\web\YiiAsset',
];
public $js = ['jquery.inputmask.bundle.min.js'];
public $depends = ['yii\web\YiiAsset'];
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment