UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

685 lines (684 loc) • 27.8 kB
/** * DevExtreme (esm/ui/number_box/number_box.mask.js) * Version: 21.1.4 * Build date: Mon Jun 21 2021 * * Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import eventsEngine from "../../events/core/events_engine"; import { extend } from "../../core/utils/extend"; import { isNumeric, isDefined, isFunction, isString } from "../../core/utils/type"; import browser from "../../core/utils/browser"; import devices from "../../core/devices"; import { fitIntoRange, inRange } from "../../core/utils/math"; import number from "../../localization/number"; import { getCaretWithOffset, isCaretInBoundaries, getCaretInBoundaries, getCaretBoundaries, getCaretAfterFormat, getCaretOffset } from "./number_box.caret"; import { getFormat as getLDMLFormat } from "../../localization/ldml/number"; import NumberBoxBase from "./number_box.base"; import { addNamespace, getChar, normalizeKeyName, isCommandKeyPressed } from "../../events/utils/index"; import { ensureDefined, escapeRegExp } from "../../core/utils/common"; import { getRealSeparatorIndex, getNthOccurrence, splitByIndex } from "./utils"; var NUMBER_FORMATTER_NAMESPACE = "dxNumberFormatter"; var MOVE_FORWARD = 1; var MOVE_BACKWARD = -1; var MINUS = "-"; var MINUS_KEY = "minus"; var NUMPUD_MINUS_KEY_IE = "Subtract"; var INPUT_EVENT = "input"; var CARET_TIMEOUT_DURATION = browser.msie ? 300 : 0; var NumberBoxMask = NumberBoxBase.inherit({ _getDefaultOptions: function() { return extend(this.callBase(), { useMaskBehavior: true, format: null }) }, _isDeleteKey: function(key) { return "del" === key }, _supportedKeys: function() { if (!this._useMaskBehavior()) { return this.callBase() } return extend(this.callBase(), { minus: this._revertSign.bind(this), del: this._removeHandler.bind(this), backspace: this._removeHandler.bind(this), leftArrow: this._arrowHandler.bind(this, MOVE_BACKWARD), rightArrow: this._arrowHandler.bind(this, MOVE_FORWARD), home: this._moveCaretToBoundaryEventHandler.bind(this, MOVE_FORWARD), enter: this._updateFormattedValue.bind(this), end: this._moveCaretToBoundaryEventHandler.bind(this, MOVE_BACKWARD) }) }, _getTextSeparatorIndex: function(text) { var decimalSeparator = number.getDecimalSeparator(); var realSeparatorOccurrenceIndex = getRealSeparatorIndex(this.option("format")).occurrence; return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex) }, _focusInHandler: function(e) { if (!this._preventNestedFocusEvent(e)) { this.clearCaretTimeout(); this._caretTimeout = setTimeout(function() { this._caretTimeout = null; var caret = this._caret(); if (caret.start === caret.end && this._useMaskBehavior()) { var text = this._getInputVal(); var decimalSeparatorIndex = this._getTextSeparatorIndex(text); if (decimalSeparatorIndex >= 0) { this._caret({ start: decimalSeparatorIndex, end: decimalSeparatorIndex }) } else { this._moveCaretToBoundaryEventHandler(MOVE_BACKWARD, e) } } }.bind(this), CARET_TIMEOUT_DURATION) } this.callBase(e) }, _focusOutHandler: function(e) { var shouldHandleEvent = !this._preventNestedFocusEvent(e); if (shouldHandleEvent) { this._focusOutOccurs = true; if (this._useMaskBehavior()) { this._updateFormattedValue() } } this.callBase(e); if (shouldHandleEvent) { this._focusOutOccurs = false } }, _hasValueBeenChanged(inputValue) { var format = this._getFormatPattern(); var value = this.option("value"); var formatted = this._format(value, format) || ""; return formatted !== inputValue }, _updateFormattedValue: function() { var inputValue = this._getInputVal(); if (this._hasValueBeenChanged(inputValue)) { this._updateParsedValue(); this._adjustParsedValue(); this._setTextByParsedValue(); if (this._parsedValue !== this.option("value")) { eventsEngine.trigger(this._input(), "change") } } }, _arrowHandler: function(step, e) { if (!this._useMaskBehavior()) { return } var text = this._getInputVal(); var format = this._getFormatPattern(); var nextCaret = getCaretWithOffset(this._caret(), step); if (!isCaretInBoundaries(nextCaret, text, format)) { nextCaret = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start; e.preventDefault(); this._caret(getCaretInBoundaries(nextCaret, text, format)) } }, _moveCaretToBoundary: function(direction) { var boundaries = getCaretBoundaries(this._getInputVal(), this._getFormatPattern()); var newCaret = getCaretWithOffset(direction === MOVE_FORWARD ? boundaries.start : boundaries.end, 0); this._caret(newCaret) }, _moveCaretToBoundaryEventHandler: function(direction, e) { if (!this._useMaskBehavior() || e && e.shiftKey) { return } this._moveCaretToBoundary(direction); e && e.preventDefault() }, _shouldMoveCaret: function(text, caret) { var decimalSeparator = number.getDecimalSeparator(); var isDecimalSeparatorNext = text.charAt(caret.end) === decimalSeparator; var isZeroNext = "0" === text.charAt(caret.end); var moveToFloat = (this._lastKey === decimalSeparator || "." === this._lastKey) && isDecimalSeparatorNext; var zeroToZeroReplace = "0" === this._lastKey && isZeroNext; return moveToFloat || zeroToZeroReplace }, _getInputVal: function() { return number.convertDigits(this._input().val(), true) }, _keyboardHandler: function(e) { this.clearCaretTimeout(); this._lastKey = number.convertDigits(getChar(e), true); this._lastKeyName = normalizeKeyName(e); if (!this._shouldHandleKey(e.originalEvent)) { return this.callBase(e) } var normalizedText = this._getInputVal(); var caret = this._caret(); var enteredChar = this._lastKeyName === MINUS_KEY ? "" : this._lastKey; var newValue = this._tryParse(normalizedText, caret, enteredChar); if (this._shouldMoveCaret(normalizedText, caret)) { this._moveCaret(1); e.originalEvent.preventDefault() } if (void 0 === newValue) { if (this._lastKeyName !== MINUS_KEY) { e.originalEvent.preventDefault() } } else { this._parsedValue = newValue } return this.callBase(e) }, _keyPressHandler: function(e) { if (!this._useMaskBehavior()) { this.callBase(e) } }, _removeHandler: function(e) { var caret = this._caret(); var text = this._getInputVal(); var start = caret.start; var end = caret.end; this._lastKey = getChar(e); this._lastKeyName = normalizeKeyName(e); var isDeleteKey = this._isDeleteKey(this._lastKeyName); var isBackspaceKey = !isDeleteKey; if (start === end) { var caretPosition = start; var canDelete = isBackspaceKey && caretPosition > 0 || isDeleteKey && caretPosition < text.length; if (canDelete) { isDeleteKey && end++; isBackspaceKey && start-- } else { e.preventDefault(); return } } var char = text.slice(start, end); if (this._isStub(char)) { this._moveCaret(isDeleteKey ? 1 : -1); if (this._parsedValue < 0 || 1 / this._parsedValue === -1 / 0) { this._revertSign(e); this._setTextByParsedValue() } e.preventDefault(); return } var decimalSeparator = number.getDecimalSeparator(); if (char === decimalSeparator) { var decimalSeparatorIndex = text.indexOf(decimalSeparator); if (this._isNonStubAfter(decimalSeparatorIndex + 1)) { this._moveCaret(isDeleteKey ? 1 : -1); e.preventDefault() } return } if (end - start < text.length) { var editedText = this._replaceSelectedText(text, { start: start, end: end }, ""); var noDigits = editedText.search(/[0-9]/) < 0; if (noDigits && this._isValueInRange(0)) { this._parsedValue = this._parsedValue < 0 || 1 / this._parsedValue === -1 / 0 ? -0 : 0; return } } var valueAfterRemoving = this._tryParse(text, { start: start, end: end }, ""); if (void 0 === valueAfterRemoving) { e.preventDefault() } else { this._parsedValue = valueAfterRemoving } }, _isPercentFormat: function() { var format = this._getFormatPattern(); var noEscapedFormat = format.replace(/'[^']+'/g, ""); return -1 !== noEscapedFormat.indexOf("%") }, _parse: function(text, format) { var formatOption = this.option("format"); var isCustomParser = isFunction(formatOption.parser); var parser = isCustomParser ? formatOption.parser : number.parse; var integerPartStartIndex = 0; if (!isCustomParser) { var formatPointIndex = getRealSeparatorIndex(format).index; var textPointIndex = this._getTextSeparatorIndex(text); var formatIntegerPartLength = -1 !== formatPointIndex ? formatPointIndex : format.length; var textIntegerPartLength = -1 !== textPointIndex ? textPointIndex : text.length; if (textIntegerPartLength > formatIntegerPartLength && -1 === format.indexOf("#")) { integerPartStartIndex = textIntegerPartLength - formatIntegerPartLength } } text = this._removeStubs(text, true); text = text.substr(integerPartStartIndex); return parser(text, format) }, _format: function(value, format) { var formatOption = this.option("format"); var isCustomFormatter = isFunction(null === formatOption || void 0 === formatOption ? void 0 : formatOption.formatter); var formatter = isCustomFormatter ? formatOption.formatter : number.format; return formatter(value, format) }, _getFormatPattern: function() { if (!this._currentFormat) { this._updateFormat() } return this._currentFormat }, _updateFormat: function() { var format = this.option("format"); var isCustomParser = isFunction(null === format || void 0 === format ? void 0 : format.parser); var isLDMLPattern = isString(format) && (format.indexOf("0") >= 0 || format.indexOf("#") >= 0); this._currentFormat = isCustomParser || isLDMLPattern ? format : getLDMLFormat(value => { var text = this._format(value, format); return number.convertDigits(text, true) }) }, _getFormatForSign: function(text) { var format = this._getFormatPattern(); if (isString(format)) { var signParts = format.split(";"); var sign = number.getSign(text, format); signParts[1] = signParts[1] || "-" + signParts[0]; return sign < 0 ? signParts[1] : signParts[0] } else { var _sign = number.getSign(text); return _sign < 0 ? "-" : "" } }, _removeStubs: function(text, excludeComma) { var format = this._getFormatForSign(text); var thousandsSeparator = number.getThousandsSeparator(); var stubs = this._getStubs(format); var result = text; if (stubs.length) { var prefixStubs = stubs[0]; var postfixRegex = new RegExp("(" + escapeRegExp(stubs[1] || "") + ")$", "g"); var decoratorsRegex = new RegExp("[-" + escapeRegExp(excludeComma ? "" : thousandsSeparator) + "]", "g"); result = result.replace(prefixStubs, "").replace(postfixRegex, "").replace(decoratorsRegex, "") } return result }, _getStubs: function(format) { var regExpResult = /[^']([#0.,]+)/g.exec(format); var pattern = regExpResult && regExpResult[0].trim(); return format.split(pattern).map((function(stub) { return stub.replace(/'/g, "") })) }, _truncateToPrecision: function(value, maxPrecision) { if (isDefined(value)) { var strValue = value.toString(); var decimalSeparatorIndex = strValue.indexOf("."); if (strValue && decimalSeparatorIndex > -1) { var parsedValue = parseFloat(strValue.substr(0, decimalSeparatorIndex + maxPrecision + 1)); return isNaN(parsedValue) ? value : parsedValue } } return value }, _tryParse: function(text, selection, char) { var editedText = this._replaceSelectedText(text, selection, char); var format = this._getFormatPattern(); var isTextSelected = selection.start !== selection.end; var parsedValue = this._getParsedValue(editedText, format); var maxPrecision = !format.parser && this._getPrecisionLimits(editedText).max; var isValueChanged = parsedValue !== this._parsedValue; var decimalSeparator = number.getDecimalSeparator(); var isDecimalPointRestricted = char === decimalSeparator && 0 === maxPrecision; var isUselessCharRestricted = !isTextSelected && !isValueChanged && char !== MINUS && !this._isValueIncomplete(editedText) && this._isStub(char); if (isDecimalPointRestricted || isUselessCharRestricted) { return } if ("" === this._removeStubs(editedText)) { parsedValue = Math.abs(0 * this._parsedValue) } if (isNaN(parsedValue)) { return } var value = null === parsedValue ? this._parsedValue : parsedValue; parsedValue = maxPrecision ? this._truncateToPrecision(value, maxPrecision) : parsedValue; return !format.parser && this._isPercentFormat() ? parsedValue && parsedValue / 100 : parsedValue }, _getParsedValue: function(text, format) { var sign = number.getSign(text, (null === format || void 0 === format ? void 0 : format.formatter) || format); var parsedValue = this._parse(text, format); var parsedValueSign = parsedValue < 0 ? -1 : 1; var parsedValueWithSign = isNumeric(parsedValue) && sign !== parsedValueSign ? sign * parsedValue : parsedValue; return parsedValueWithSign }, _isValueIncomplete: function(text) { if (!this._useMaskBehavior()) { return this.callBase(text) } var caret = this._caret(); var point = number.getDecimalSeparator(); var pointIndex = this._getTextSeparatorIndex(text); var isCaretOnFloat = pointIndex >= 0 && pointIndex < caret.start; var textParts = this._removeStubs(text, true).split(point); if (!isCaretOnFloat || 2 !== textParts.length) { return false } var floatLength = textParts[1].length; var format = this._getFormatPattern(); var isCustomParser = !!format.parser; var precision = !isCustomParser && this._getPrecisionLimits(this._getFormatPattern(), text); var isPrecisionInRange = isCustomParser ? true : inRange(floatLength, precision.min, precision.max); var endsWithZero = "0" === textParts[1].charAt(floatLength - 1); return isPrecisionInRange && (endsWithZero || !floatLength) }, _isValueInRange: function(value) { var min = ensureDefined(this.option("min"), -1 / 0); var max = ensureDefined(this.option("max"), 1 / 0); return inRange(value, min, max) }, _setInputText: function(text) { var normalizedText = number.convertDigits(text, true); var newCaret = getCaretAfterFormat(this._getInputVal(), normalizedText, this._caret(), this._getFormatPattern()); this._input().val(text); this._toggleEmptinessEventHandler(); this._formattedValue = text; if (!this._focusOutOccurs) { this._caret(newCaret) } }, _useMaskBehavior: function() { return !!this.option("format") && this.option("useMaskBehavior") }, _renderInputType: function() { var isNumberType = "number" === this.option("mode"); var isDesktop = "desktop" === devices.real().deviceType; if (this._useMaskBehavior() && isNumberType) { this._setInputType(isDesktop || this._isSupportInputMode() ? "text" : "tel") } else { this.callBase() } }, _isChar: function(str) { return isString(str) && 1 === str.length }, _moveCaret: function(offset) { if (!offset) { return } var newCaret = getCaretWithOffset(this._caret(), offset); var adjustedCaret = getCaretInBoundaries(newCaret, this._getInputVal(), this._getFormatPattern()); this._caret(adjustedCaret) }, _shouldHandleKey: function(e) { var keyName = normalizeKeyName(e); var isSpecialChar = isCommandKeyPressed(e) || e.altKey || e.shiftKey || !this._isChar(keyName); var isMinusKey = keyName === MINUS_KEY; var useMaskBehavior = this._useMaskBehavior(); return useMaskBehavior && !isSpecialChar && !isMinusKey }, _renderInput: function() { this.callBase(); this._renderFormatter() }, _renderFormatter: function() { this._clearCache(); this._detachFormatterEvents(); if (this._useMaskBehavior()) { this._attachFormatterEvents() } }, _detachFormatterEvents: function() { eventsEngine.off(this._input(), "." + NUMBER_FORMATTER_NAMESPACE) }, _isInputFromPaste: function(e) { var inputType = e.originalEvent && e.originalEvent.inputType; if (isDefined(inputType)) { return "insertFromPaste" === inputType } else { return this._isValuePasted } }, _attachFormatterEvents: function() { var $input = this._input(); eventsEngine.on($input, addNamespace(INPUT_EVENT, NUMBER_FORMATTER_NAMESPACE), function(e) { this._formatValue(e); this._isValuePasted = false }.bind(this)); if (browser.msie && browser.version < 12) { eventsEngine.on($input, addNamespace("paste", NUMBER_FORMATTER_NAMESPACE), function() { this._isValuePasted = true }.bind(this)) } eventsEngine.on($input, addNamespace("dxclick", NUMBER_FORMATTER_NAMESPACE), function() { if (!this._caretTimeout) { this._caretTimeout = setTimeout(function() { this._caret(getCaretInBoundaries(this._caret(), this._getInputVal(), this._getFormatPattern())) }.bind(this), CARET_TIMEOUT_DURATION) } }.bind(this)); eventsEngine.on($input, "dxdblclick", function() { this.clearCaretTimeout() }.bind(this)) }, clearCaretTimeout: function() { clearTimeout(this._caretTimeout); this._caretTimeout = null }, _forceRefreshInputValue: function() { if (!this._useMaskBehavior()) { return this.callBase() } }, _isNonStubAfter: function(index) { var text = this._getInputVal().slice(index); return text && !this._isStub(text, true) }, _isStub: function(str, isString) { var escapedDecimalSeparator = escapeRegExp(number.getDecimalSeparator()); var regExpString = "^[^0-9" + escapedDecimalSeparator + "]+$"; var stubRegExp = new RegExp(regExpString, "g"); return stubRegExp.test(str) && (isString || this._isChar(str)) }, _parseValue: function(text) { if (!this._useMaskBehavior()) { return this.callBase(text) } return this._parsedValue }, _getPrecisionLimits: function(text) { var currentFormat = this._getFormatForSign(text); var realSeparatorIndex = getRealSeparatorIndex(currentFormat).index; var floatPart = (splitByIndex(currentFormat, realSeparatorIndex)[1] || "").replace(/[^#0]/g, ""); var minPrecision = floatPart.replace(/^(0*)#*/, "$1").length; var maxPrecision = floatPart.length; return { min: minPrecision, max: maxPrecision } }, _revertSign: function(e) { if (!this._useMaskBehavior()) { return } var caret = this._caret(); if (caret.start !== caret.end) { if (normalizeKeyName(e) === MINUS_KEY) { this._applyRevertedSign(e, caret, true); return } else { this._caret(getCaretInBoundaries(0, this._getInputVal(), this._getFormatPattern())) } } this._applyRevertedSign(e, caret) }, _applyRevertedSign: function(e, caret, preserveSelectedText) { var newValue = -1 * ensureDefined(this._parsedValue, null); if (this._isValueInRange(newValue)) { this._parsedValue = newValue; if (preserveSelectedText) { var format = this._getFormatPattern(); var previousText = this._getInputVal(); this._setTextByParsedValue(); e.preventDefault(); var currentText = this._getInputVal(); var offset = getCaretOffset(previousText, currentText, format); caret = getCaretWithOffset(caret, offset); var caretInBoundaries = getCaretInBoundaries(caret, currentText, format); if (browser.msie) { clearTimeout(this._caretTimeout); this._caretTimeout = setTimeout(this._caret.bind(this, caretInBoundaries)) } else { this._caret(caretInBoundaries) } } if (e.key === NUMPUD_MINUS_KEY_IE) { eventsEngine.trigger(this._input(), INPUT_EVENT) } } }, _removeMinusFromText: function(text, caret) { var isMinusPressed = this._lastKeyName === MINUS_KEY && text.charAt(caret.start - 1) === MINUS; return isMinusPressed ? this._replaceSelectedText(text, { start: caret.start - 1, end: caret.start }, "") : text }, _setTextByParsedValue: function() { var format = this._getFormatPattern(); var parsed = this._parseValue(); var formatted = this._format(parsed, format) || ""; this._setInputText(formatted) }, _formatValue: function(e) { var normalizedText = this._getInputVal(); var caret = this._caret(); var textWithoutMinus = this._removeMinusFromText(normalizedText, caret); var wasMinusRemoved = textWithoutMinus !== normalizedText; normalizedText = textWithoutMinus; if (!this._isInputFromPaste(e) && this._isValueIncomplete(textWithoutMinus)) { this._formattedValue = normalizedText; if (wasMinusRemoved) { this._setTextByParsedValue() } return } var textWasChanged = number.convertDigits(this._formattedValue, true) !== normalizedText; if (textWasChanged) { var value = this._tryParse(normalizedText, caret, ""); if (isDefined(value)) { this._parsedValue = value } } this._setTextByParsedValue() }, _renderDisplayText: function() { if (this._useMaskBehavior()) { this._toggleEmptinessEventHandler() } else { this.callBase.apply(this, arguments) } }, _renderValue: function() { if (this._useMaskBehavior()) { this._parsedValue = this.option("value"); this._setTextByParsedValue() } return this.callBase() }, _updateParsedValue: function() { var inputValue = this._getInputVal(); this._parsedValue = this._tryParse(inputValue, this._caret()) }, _adjustParsedValue: function() { if (!this._useMaskBehavior()) { return } var clearedText = this._removeStubs(this._getInputVal()); var parsedValue = clearedText ? this._parseValue() : null; if (!isNumeric(parsedValue)) { this._parsedValue = parsedValue; return } this._parsedValue = fitIntoRange(parsedValue, this.option("min"), this.option("max")) }, _valueChangeEventHandler: function(e) { if (!this._useMaskBehavior()) { return this.callBase(e) } var caret = this._caret(); this._saveValueChangeEvent(e); this._lastKey = null; this._lastKeyName = null; this._updateParsedValue(); this._adjustParsedValue(); this.option("value", this._parsedValue); if (caret) { this._caret(caret) } }, _optionChanged: function(args) { switch (args.name) { case "format": case "useMaskBehavior": this._renderInputType(); this._updateFormat(); this._renderFormatter(); this._renderValue(); this._refreshValueChangeEvent(); this._refreshEvents(); break; case "min": case "max": this._adjustParsedValue(); this.callBase(args); break; default: this.callBase(args) } }, _clearCache: function() { delete this._formattedValue; delete this._lastKey; delete this._lastKeyName; delete this._parsedValue; delete this._focusOutOccurs; clearTimeout(this._caretTimeout); delete this._caretTimeout }, _clean: function() { this._clearCache(); this.callBase() } }); export default NumberBoxMask;