/** * Input masks provide a way for developers to define rules that govern user input. This ensures * that data is submitted in an expected format and with the appropriate character set. * * Frequent uses of input masks include: * * + Zip or postal codes * + Times * + Dates * + Telephone numbers * * ## Character Sets * * Input mask characters can be defined by representations of the desired set. For instance, * if you only want to allow numbers, you can use 0 or 9. Here is the list of default * representations: * * + '*': '[A-Za-z0-9]' // any case letter A-Z, any integer * + 'a': '[a-z]' // any lower case letter a-z * + 'A': '[A-Z]' // any upper case letter A-Z * + '0': '[0-9]' // any integer * + '9': '[0-9]' // any integer * * So, to create a telephone input mask, you could use: * * + (000) 000-0000 * * or * * + (999) 999-9999 * * ## Telephone input mask * * @example toolkit=modern * Ext.create({ * fullscreen: true, * xtype: 'formpanel', * * items: [{ * xtype: 'textfield', * label: 'Phone Number', * placeholder: '(xxx) xxx-xxxx', * inputMask: '(999) 999-9999' * }] * }); */Ext.define('Ext.field.InputMask', function (InputMask) { return { requires: [ 'Ext.util.LRU' ], cachedConfig: { blank: '_', characters: { '*': '[A-Za-z0-9]', 'a': '[a-z]', 'A': '[A-Z]', '0': '[0-9]', '9': '[0-9]' }, ignoreCase: true }, config: { /** * @cfg {String} pattern (required) */ pattern: null }, _cached: false, _lastEditablePos: null, _mask: null, statics: { active: {}, from: function (value, existing) { var active = InputMask.active, ret; if (value === null) { ret = null; } else if (typeof value !== 'string') { if (existing && !existing._cached) { ret = existing; ret.setConfig(value); } else { ret = new InputMask(value); } } else if (!(ret = active[value])) { // No one is currently using this mask, but check the cache of // recently used masks. We remove the mask from the cache and // move it to the active set... if it was there. if (!(ret = InputMask.cache.remove(value))) { ret = new InputMask({ pattern: value }); } active[value] = ret; ret._cached = 1; // this is the first user either way } else { // The mask was found in the active set so we can reuse it // (just bump the counter). ++ret._cached; } return ret; } }, constructor: function (config) { this.initConfig(config); }, release: function () { var me = this, cache = InputMask.cache, key; if (me._cached && !--me._cached) { key = me.getPattern(); //<debug> if (InputMask.active[key] !== me) { Ext.raise('Invalid call to InputMask#release (not active)'); } if (cache.map[key]) { Ext.raise('Invalid call to InputMask#release (already cached)'); } //</debug> delete InputMask.active[key]; cache.add(key, me); cache.trim(cache.maxSize); } //<debug> else if (me._cached === 0) { Ext.raise('Invalid call to InputMask#release (already released)'); } //</debug> }, clearRange: function (value, start, len) { var me = this, blank = me.getBlank(), end = start + len, n = value.length, s = '', i, mask, prefixLen; if (!blank) { prefixLen = me._prefix.length; for (i = 0; i < n; ++i) { if (i < prefixLen || i < start || i >= end) { s += value[i]; } } s = me.formatValue(s); } else { mask = me.getPattern(); for (i = 0; i < n; ++i) { if (i < start || i >= end) { s += value[i]; } else if (me.isFixedChar(i)) { s += mask[i]; } else { s += blank; } } } return s; }, formatValue: function (value) { var me = this, blank = me.getBlank(), i, length, mask, prefix, s; if (!blank) { prefix = me._prefix; length = prefix.length; s = this.insertRange('', value, 0); for (i = s.length; i > length && me.isFixedChar(i - 1);) { --i; } s = (i < length) ? prefix : s.slice(0, i - 1); } else if (value) { s = me.formatValue(''); s = me.insertRange(s, value, 0); } else { mask = me.getPattern(); s = ''; for (i = 0, length = mask.length; i < length; ++i) { if (me.isFixedChar(i)) { s += mask[i]; } else { s += blank; } } } return s; }, getEditPosLeft: function (pos) { for (var i = pos; i >= 0; --i) { if (!this.isFixedChar(i)) { return i; } } return null; }, getEditPosRight: function (pos) { var mask = this._mask, len = mask.length, i; for (i = pos; i < len; ++i) { if (!this.isFixedChar(i)) { return i; } } return null; }, getFilledLength: function (value) { var me = this, blank = me.getBlank(), c, i; if (!blank) { return value.length; } for (i = value && value.length; i-- > 0;) { c = value[i]; if (!me.isFixedChar(i) && me.isAllowedChar(c, i)) { break; } } return ++i || me._prefix.length; }, getSubLength: function (value, substr, pos) { var me = this, mask = me.getPattern(), k = 0, maskLen = mask.length, substrLen = substr.length, i; for (i = pos; i < maskLen && k < substrLen;) { if (!me.isFixedChar(i) || mask[i] === substr[k]) { if (me.isAllowedChar(substr[k++], i, true)) { ++i; } } else { ++i; } } return i - pos; }, insertRange: function (value, substr, pos) { var me = this, mask = me.getPattern(), blank = me.getBlank(), filled = me.isFilled(value), prefixLen = me._prefix.length, maskLen = mask.length, substrLen = substr.length, s = value, ch, fixed, i, k; if (!blank && pos > s.length) { s += mask.slice(s.length, pos); } for (i = pos, k = 0; i < maskLen && k < substrLen;) { fixed = me.isFixedChar(i); if (!fixed || mask[i] === substr[k]) { ch = substr[k++]; if (me.isAllowedChar(ch, i, true)) { if (i < s.length) { if (blank || filled || i < prefixLen) { s = s.slice(0, i) + ch + s.slice(i + 1); } else { s = me.formatValue(s.substr(0, i) + ch + s.substr(i)); } } else if (!blank) { s += ch; } ++i; } } else { if (!blank && i >= s.length) { s += mask[i]; } else if (blank && fixed && substr[k] === blank) { ++k; } ++i; } } return s; }, isAllowedChar: function (character, pos, allowBlankChar) { var me = this, mask = me.getPattern(), c, characters, rule; if (me.isFixedChar(pos)) { return mask[pos] === character; } c = mask[pos]; characters = me.getCharacters(); rule = characters[c]; return !rule || rule.test(character || '') || (allowBlankChar && character === me.getBlank()); }, isEmpty: function (value) { for (var i = 0, len = value.length; i < len; ++i) { if (!this.isFixedChar(i) && this.isAllowedChar(value[i], i)) { return false; } } return true; }, // TODO This function would benefit from optimization // Used during validation and range insert isFilled: function (value) { return this.getFilledLength(value) === this._mask.length; }, isFixedChar: function (pos) { return Ext.Array.indexOf(this._fixedCharPositions, pos) > -1; }, setCaretToEnd: function (field, value) { var filledLen = this.getFilledLength(value), pos = this.getEditPosRight(filledLen); if (pos !== null) { // Because we are called during a focus event, we have to delay pushing // down the new caret position to the next frame or else the browser will // position the caret at the end of the text. Note, Ext.asap() does *not* // work reliably for this. Ext.raf(function () { if (!field.destroyed) { field.setCaretPos(pos); Ext.raf(function () { if (!field.destroyed) { field.setCaretPos(pos); } }); } }); } }, //--------------------------------------------------------------------- // Event Handling onBlur: function (field, value) { if (field.getAutoHideInputMask() !== false) { if (this.isEmpty(value)) { field.maskProcessed = true; field.setValue(''); } } }, onFocus: function (field, value) { // On focus we have to show the mask and move caret to the last editable position // If field has autoHideInputMask === false, inputMask is always shown so we only move the caret if (field.getAutoHideInputMask() !== false) { if (!value) { field.maskProcessed = true; field.setValue(this._mask); } } this.setCaretToEnd(field, value); }, onChange: function (field, value, oldValue) { var me = this, s; if (field.maskProcessed || value === oldValue) { field.maskProcessed = false; return true; } if (value) { s = me.formatValue(value); field.maskProcessed = true; field.setValue(s); } }, processAutocomplete: function (field, value) { var me = this, s; if (value) { if (value.length > me._mask.length) { value = value.substr(0, me._mask.length); } s = me.formatValue(value); field.maskProcessed = true; field.inputElement.dom.value = s; //match DOM field.setValue(s); this.setCaretToEnd(field, value); } }, /** * @private * @param field * @param adjustCaret {Boolean} move caret to the first editable position */ showEmptyMask: function (field, adjustCaret) { var s = this.formatValue(); field.maskProcessed = true; field.setValue(s); if (adjustCaret) { this.setCaretToEnd(field); } }, onKeyDown: function (field, value, event) { if (event.ctrlKey || event.metaKey) { return; } var me = this, //key = event.key(), // Does not work on mobile key = event.keyCode === event.DELETE, del = key === 'Delete', handled = del || (event.keyCode === event.BACKSPACE), s = value, caret, editPos, len, prefixLen, textSelection, start; if (handled) { caret = field.getCaretPos(); prefixLen = me._prefix.length; textSelection = field.getTextSelection(); start = textSelection[0]; len = textSelection[1] - start; if (len) { s = me.clearRange(value, start, len); } else if (caret < prefixLen || (!del && caret === prefixLen)) { caret = prefixLen; } else { editPos = del ? me.getEditPosRight(caret) : me.getEditPosLeft(caret - 1); if (editPos !== null) { s = me.clearRange(value, editPos, 1); caret = editPos; } } if (s !== value) { field.maskProcessed = true; field.setValue(s); } event.preventDefault(); field.setCaretPos(caret); } }, onKeyPress: function (field, value, event) { var me = this, key = event.keyCode, ch = event.getChar(), mask = me.getPattern(), prefixLen = me._prefix.length, s = value, caretPos, pos, start, textSelection; if (key === event.ENTER || key === event.TAB || event.ctrlKey || event.metaKey) { return; } //TODO Windows Phone may need to return here caretPos = field.getCaretPos(); textSelection = field.getTextSelection(); if (me.isFixedChar(caretPos) && mask[caretPos] === ch) { s = me.insertRange(s, ch, caretPos); ++caretPos; } else { pos = me.getEditPosRight(caretPos); if (pos !== null && me.isAllowedChar(ch, pos)) { start = textSelection[0]; s = me.clearRange(s, start, textSelection[1] - start); s = me.insertRange(s, ch, pos); caretPos = pos + 1; } } if (s !== value) { field.maskProcessed = true; field.setValue(s); } event.preventDefault(); if (caretPos < me._lastEditablePos && caretPos > prefixLen) { caretPos = me.getEditPosRight(caretPos); } field.setCaretPos(caretPos); }, onPaste: function (field, value, event) { // TODO Android browser issues // https://bugs.chromium.org/p/chromium/issues/detail?id=369101 var text, clipdData = event.browserEvent.clipboardData; if (clipdData && clipdData.getData) { text = clipdData.getData('text/plain'); } else if (Ext.global.clipboardData && Ext.global.clipboardData.getData) { text = Ext.global.clipboardData.getData('Text'); // IE } if (text) { this.paste(field, value, text, field.getTextSelection()); } event.preventDefault(); }, paste: function (field, value, text, selection) { var me = this, caretPos = selection[0], len = selection[1] - caretPos, s = len ? me.clearRange(value, caretPos, len) : value, textLen = me.getSubLength(s, text, caretPos); s = me.insertRange(s, text, caretPos); caretPos += textLen; caretPos = me.getEditPosRight(caretPos) || caretPos; if (s !== value) { field.maskProcessed = true; field.setValue(s); } field.setCaretPos(caretPos); }, syncPattern: function (field) { var fieldValue = field.getValue(), s; if (field.getAutoHideInputMask() === false) { //show blank input mask if there is no initial value if (!fieldValue) { this.showEmptyMask(field); } else { //format any value and combine with mask s = this.formatValue(fieldValue); field.maskProcessed = true; field.setValue(s); } } else { //field has auto hide mask, but there might be an initial value //don't process empty value as that will set value to match the mask if (fieldValue) { s = this.formatValue(fieldValue); field.maskProcessed = true; field.setValue(s); } } }, //--------------------------------------------------------------------- // Configs applyCharacters: function (map) { var ret = {}, flags = this.getIgnoreCase() ? 'i' : '', c, v; for (c in map) { v = map[c]; if (typeof v === 'string') { v = new RegExp(v, flags); } ret[c] = v; } return ret; }, updatePattern: function (mask) { var me = this, characters = me.getCharacters(), lastEditablePos = 0, n = mask && mask.length, blank = me.getBlank(), fixedPosArr = [], prefix = '', str = '', c, i; for (i = 0; i < n; ++i) { c = mask[i]; if (!characters[c]) { fixedPosArr.push(str.length); str += c; } else { lastEditablePos = str.length + 1; str += blank; } } me._lastEditablePos = lastEditablePos; me._mask = str; me._fixedCharPositions = fixedPosArr; //Now that _fixedCharPositions are populated, isFixedChar can be used for (i = 0; i < str.length && me.isFixedChar(i); ++i) { prefix += str[i]; } me._prefix = prefix; }};},function (InputMask) { InputMask.cache = new Ext.util.LRU(); InputMask.cache.maxSize = 100;});