devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
606 lines (605 loc) • 21.2 kB
JavaScript
/**
* DevExtreme (esm/__internal/ui/text_box/text_editor.mask.js)
* Version: 25.2.7
* Build date: Tue May 05 2026
*
* Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import eventsEngine from "../../../common/core/events/core/events_engine";
import {
name as wheelEventName
} from "../../../common/core/events/core/wheel";
import {
addNamespace,
createEvent,
isCommandKeyPressed,
normalizeKeyName
} from "../../../common/core/events/utils/index";
import messageLocalization from "../../../common/core/localization/message";
import $ from "../../../core/renderer";
import {
extend
} from "../../../core/utils/extend";
import {
isEmpty
} from "../../../core/utils/string";
import {
isDefined
} from "../../../core/utils/type";
import {
focused
} from "../../core/utils/m_selectors";
import TextEditorBase from "../../ui/text_box/m_text_editor.base";
import {
EmptyMaskRule,
MaskRule,
StubMaskRule
} from "../../ui/text_box/text_editor.mask.rule";
import MaskStrategy from "../../ui/text_box/text_editor.mask.strategy";
import caretUtils from "../../ui/text_box/utils.caret";
const EMPTY_CHAR = " ";
const ESCAPED_CHAR = "\\";
const TEXTEDITOR_MASKED_CLASS = "dx-texteditor-masked";
const FORWARD_DIRECTION = "forward";
const BACKWARD_DIRECTION = "backward";
const DROP_EVENT_NAME = "drop";
const isNumericChar = char => /[0-9]/.test(char);
const isLiteralChar = char => {
const code = char.charCodeAt(0);
return code > 64 && code < 91 || code > 96 && code < 123 || code > 127
};
const isSpaceChar = char => " " === char;
const buildInMaskRules = {
0: /[0-9]/,
9: /[0-9\s]/,
"#": /[-+0-9\s]/,
L: char => isLiteralChar(char),
l: char => isLiteralChar(char) || isSpaceChar(char),
C: /\S/,
c: /./,
A: char => isLiteralChar(char) || isNumericChar(char),
a: char => isLiteralChar(char) || isNumericChar(char) || isSpaceChar(char)
};
class TextEditorMask extends TextEditorBase {
_getDefaultOptions() {
return Object.assign({}, super._getDefaultOptions(), {
mask: "",
maskChar: "_",
maskRules: {},
maskInvalidMessage: messageLocalization.format("validation-mask"),
useMaskedValue: false,
showMaskMode: "always"
})
}
_supportedKeys() {
const result = super._supportedKeys();
const keyHandlerMap = {
del: this._maskStrategy.getHandler("del"),
enter: this._changeHandler
};
Object.entries(keyHandlerMap).forEach(_ref => {
let [key, handler] = _ref;
const parentHandler = result[key];
result[key] = e => {
const {
mask: mask
} = this.option();
if (mask && handler) {
handler.call(this, e)
}
null === parentHandler || void 0 === parentHandler || parentHandler(e)
}
});
return result
}
_getSubmitElement() {
const {
mask: mask
} = this.option();
const submitElement = !mask ? super._getSubmitElement() : this._$hiddenElement;
return submitElement
}
_init() {
super._init();
this._initMaskStrategy()
}
_initMaskStrategy() {
this._maskStrategy = new MaskStrategy(this)
}
_initMarkup() {
this._renderHiddenElement();
super._initMarkup()
}
_attachMouseWheelEventHandlers() {
if (!this._hasMouseWheelHandler()) {
return
}
const input = this._input();
const eventName = addNamespace(wheelEventName, this.NAME);
const mouseWheelAction = this._createAction(e => {
const {
event: event
} = e;
if (focused(input) && !isCommandKeyPressed(event)) {
this._onMouseWheel(event);
event.preventDefault();
event.stopPropagation()
}
});
eventsEngine.off(input, eventName);
eventsEngine.on(input, eventName, e => {
mouseWheelAction({
event: e
})
})
}
_hasMouseWheelHandler() {
return false
}
_onMouseWheel(e) {}
_useMaskBehavior() {
const {
mask: mask
} = this.option();
return Boolean(mask)
}
_attachDropEventHandler() {
const useMaskBehavior = this._useMaskBehavior();
if (!useMaskBehavior) {
return
}
const eventName = addNamespace("drop", this.NAME);
const input = this._input();
eventsEngine.off(input, eventName);
eventsEngine.on(input, eventName, e => {
e.preventDefault()
})
}
_render() {
this._attachMouseWheelEventHandlers();
this._renderMask();
super._render();
this._attachDropEventHandler()
}
_renderHiddenElement() {
const {
mask: mask
} = this.option();
if (mask) {
this._$hiddenElement = $("<input>").attr("type", "hidden").appendTo(this._inputWrapper())
}
}
_removeHiddenElement() {
var _this$_$hiddenElement;
null === (_this$_$hiddenElement = this._$hiddenElement) || void 0 === _this$_$hiddenElement || _this$_$hiddenElement.remove()
}
_renderMask() {
this.$element().removeClass("dx-texteditor-masked");
this._maskRulesChain = null;
this._maskStrategy.detachEvents();
const {
mask: mask
} = this.option();
if (!mask) {
return
}
this.$element().addClass("dx-texteditor-masked");
this._maskStrategy.attachEvents();
this._parseMask();
this._renderMaskedValue()
}
_changeHandler(e) {
const $input = this._input();
const inputValue = $input.val();
if (inputValue === this._changedValue) {
return
}
this._changedValue = inputValue;
const changeEvent = createEvent(e, {
type: "change"
});
eventsEngine.trigger($input, changeEvent)
}
_parseMask() {
const {
maskRules: maskRules
} = this.option();
this._maskRules = extend({}, buildInMaskRules, maskRules);
this._maskRulesChain = this._parseMaskRule(0)
}
_parseMaskRule(index) {
const {
mask: mask
} = this.option();
if (!isDefined(mask) || index >= mask.length) {
return new EmptyMaskRule({})
}
const currentMaskChar = mask[index];
const isEscapedChar = "\\" === currentMaskChar;
const result = isEscapedChar ? new StubMaskRule({
maskChar: mask[index + 1]
}) : this._getMaskRule(currentMaskChar);
const nextIndex = index + 1 + Number(isEscapedChar);
const recursiveResult = this._parseMaskRule(nextIndex);
result.next(recursiveResult);
return result
}
_getMaskRule(pattern) {
if (!this._maskRules) {
return new StubMaskRule({
maskChar: pattern
})
}
const matchingEntry = Object.entries(this._maskRules).find(_ref2 => {
let [rulePattern] = _ref2;
return rulePattern === pattern
});
if (matchingEntry) {
const [, allowedChars] = matchingEntry;
const ruleConfig = {
pattern: pattern,
allowedChars: allowedChars
};
const {
maskChar: maskChar
} = this.option();
return new MaskRule(extend({
maskChar: maskChar || " "
}, ruleConfig))
}
return new StubMaskRule({
maskChar: pattern
})
}
_renderMaskedValue() {
if (!this._maskRulesChain) {
return
}
const {
value: optionValue
} = this.option();
const value = optionValue || "";
this._maskRulesChain.clear(this._normalizeChainArguments());
const chainArgs = {
length: null === value || void 0 === value ? void 0 : value.length
};
const prop = this._isMaskedValueMode() ? "text" : "value";
chainArgs[prop] = value;
this._handleChain(chainArgs);
this._displayMask()
}
_replaceSelectedText(text, selection, char) {
if (void 0 === char) {
return text
}
const textBefore = text.slice(0, selection.start);
const textAfter = text.slice(selection.end);
const edited = `${textBefore}${char}${textAfter}`;
return edited
}
_isMaskedValueMode() {
const {
useMaskedValue: useMaskedValue
} = this.option();
return Boolean(useMaskedValue)
}
_displayMask(caret) {
const currentCaret = caret ?? this._caret();
const finalCaret = {
start: (null === currentCaret || void 0 === currentCaret ? void 0 : currentCaret.start) ?? 0,
end: (null === currentCaret || void 0 === currentCaret ? void 0 : currentCaret.end) ?? 0
};
this._renderValue();
this._caret(finalCaret)
}
_isValueEmpty() {
return isEmpty(this._value)
}
_shouldShowMask() {
const {
showMaskMode: showMaskMode
} = this.option();
if ("onFocus" === showMaskMode) {
return focused(this._input()) || !this._isValueEmpty()
}
return true
}
_showMaskPlaceholder() {
if (this._shouldShowMask()) {
var _this$_maskRulesChain;
const text = null === (_this$_maskRulesChain = this._maskRulesChain) || void 0 === _this$_maskRulesChain ? void 0 : _this$_maskRulesChain.text();
this.option({
text: text
});
const {
showMaskMode: showMaskMode
} = this.option();
if ("onFocus" === showMaskMode) {
this._renderDisplayText(text)
}
}
}
_renderValue() {
if (this._maskRulesChain) {
this._showMaskPlaceholder();
if (this._$hiddenElement) {
const value = this._maskRulesChain.value();
const submitElementValue = !isEmpty(value) ? this._getPreparedValue() : "";
this._$hiddenElement.val(submitElementValue)
}
}
return super._renderValue()
}
_getPreparedValue() {
return this._convertToValue().replace(/\s+$/, "")
}
_valueChangeEventHandler() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key]
}
if (!this._maskRulesChain) {
super._valueChangeEventHandler(...args);
return
}
const [e] = args;
this._saveValueChangeEvent(e);
const preparedValue = this._getPreparedValue();
this.option({
value: preparedValue
})
}
_isControlKeyFired(e) {
const normalizedKeyName = normalizeKeyName(e);
const isControlKey = isDefined(normalizedKeyName) ? this._isControlKey(normalizedKeyName) : false;
return isControlKey || isCommandKeyPressed(e)
}
_handleChain(args) {
var _this$_maskRulesChain2;
const handledCount = (null === (_this$_maskRulesChain2 = this._maskRulesChain) || void 0 === _this$_maskRulesChain2 ? void 0 : _this$_maskRulesChain2.handle(this._normalizeChainArguments(args))) ?? 0;
this._updateMaskInfo();
return handledCount
}
_normalizeChainArguments(args) {
var _this$_maskRulesChain3;
return Object.assign({}, args, {
index: 0,
fullText: null === (_this$_maskRulesChain3 = this._maskRulesChain) || void 0 === _this$_maskRulesChain3 ? void 0 : _this$_maskRulesChain3.text()
})
}
_convertToValue(text) {
if (this._isMaskedValueMode()) {
return this._replaceMaskCharWithEmpty(text || this._textValue || "")
}
return text || this._value || ""
}
_replaceMaskCharWithEmpty(text) {
const {
maskChar: maskChar
} = this.option();
return text.replace(new RegExp(maskChar, "g"), " ")
}
_maskKeyHandler(e, keyHandler) {
const {
readOnly: readOnly
} = this.option();
if (readOnly) {
return
}
this.setForwardDirection();
e.preventDefault();
this._handleSelection();
const previousText = this._input().val();
const raiseInputEvent = () => {
if (previousText !== this._input().val()) {
eventsEngine.trigger(this._input(), "input")
}
};
const handled = keyHandler();
if (handled) {
handled.then(raiseInputEvent)
} else {
var _this$_maskRulesChain4;
this.setForwardDirection();
this._adjustCaret();
this._displayMask();
null === (_this$_maskRulesChain4 = this._maskRulesChain) || void 0 === _this$_maskRulesChain4 || _this$_maskRulesChain4.reset();
raiseInputEvent()
}
}
_handleKey(key, direction) {
this._direction(direction || "forward");
this._adjustCaret(key);
this._handleKeyChain(key);
this._moveCaret()
}
_handleSelection() {
if (!this._hasSelection()) {
return
}
const caret = this._caret();
const caretStart = (null === caret || void 0 === caret ? void 0 : caret.start) ?? 0;
const caretEnd = (null === caret || void 0 === caret ? void 0 : caret.end) ?? 0;
const emptyChars = new Array(caretEnd - caretStart + 1).join(" ");
this._handleKeyChain(emptyChars)
}
_handleKeyChain(chars) {
const caret = this._caret();
const caretStart = (null === caret || void 0 === caret ? void 0 : caret.start) ?? 0;
const caretEnd = (null === caret || void 0 === caret ? void 0 : caret.end) ?? 0;
const start = this.isForwardDirection() ? caretStart : caretStart - 1;
const end = this.isForwardDirection() ? caretEnd : caretEnd - 1;
const length = start === end ? 1 : end - start;
this._handleChain({
text: chars,
start: start,
length: length
})
}
_tryMoveCaretBackward() {
var _this$_caret, _this$_caret2;
this.setBackwardDirection();
const currentCaret = null === (_this$_caret = this._caret()) || void 0 === _this$_caret ? void 0 : _this$_caret.start;
this._adjustCaret();
return !currentCaret || currentCaret !== (null === (_this$_caret2 = this._caret()) || void 0 === _this$_caret2 ? void 0 : _this$_caret2.start)
}
_adjustCaret(char) {
var _this$_caret3, _this$_maskRulesChain5;
const caretStart = (null === (_this$_caret3 = this._caret()) || void 0 === _this$_caret3 ? void 0 : _this$_caret3.start) ?? 0;
const isForwardDirection = this.isForwardDirection();
const caret = null === (_this$_maskRulesChain5 = this._maskRulesChain) || void 0 === _this$_maskRulesChain5 ? void 0 : _this$_maskRulesChain5.adjustedCaret(caretStart, isForwardDirection, char ?? "");
this._caret({
start: caret,
end: caret
})
}
_moveCaret() {
var _this$_caret4, _this$_maskRulesChain6;
const currentCaret = (null === (_this$_caret4 = this._caret()) || void 0 === _this$_caret4 ? void 0 : _this$_caret4.start) ?? 0;
const maskRuleIndex = currentCaret + (this.isForwardDirection() ? 0 : -1);
const caret = null !== (_this$_maskRulesChain6 = this._maskRulesChain) && void 0 !== _this$_maskRulesChain6 && _this$_maskRulesChain6.isAccepted(maskRuleIndex) ? currentCaret + (this.isForwardDirection() ? 1 : -1) : currentCaret;
this._caret({
start: caret,
end: caret
})
}
_caret(position, force) {
const $input = this._input();
if (!$input.length) {
return
}
if (arguments.length > 0) {
caretUtils($input, position, force);
return
}
return caretUtils($input)
}
_hasSelection() {
const caret = this._caret();
return (null === caret || void 0 === caret ? void 0 : caret.start) !== (null === caret || void 0 === caret ? void 0 : caret.end)
}
_direction(direction) {
if (!arguments.length) {
return this._typingDirection
}
this._typingDirection = direction
}
setForwardDirection() {
this._direction("forward")
}
setBackwardDirection() {
this._direction("backward")
}
isForwardDirection() {
return "forward" === this._direction()
}
_updateMaskInfo() {
var _this$_maskRulesChain7, _this$_maskRulesChain8;
this._textValue = null === (_this$_maskRulesChain7 = this._maskRulesChain) || void 0 === _this$_maskRulesChain7 ? void 0 : _this$_maskRulesChain7.text();
this._value = null === (_this$_maskRulesChain8 = this._maskRulesChain) || void 0 === _this$_maskRulesChain8 ? void 0 : _this$_maskRulesChain8.value()
}
_clean() {
var _this$_maskStrategy;
null === (_this$_maskStrategy = this._maskStrategy) || void 0 === _this$_maskStrategy || _this$_maskStrategy.clean();
super._clean()
}
_validateMask() {
if (!this._maskRulesChain) {
return
}
const {
maskInvalidMessage: maskInvalidMessage,
value: value
} = this.option();
const defaultValidationError = {
editorSpecific: true,
message: maskInvalidMessage
};
const isValid = isEmpty(value) || this._maskRulesChain.isValid(this._normalizeChainArguments());
const validationError = isValid ? null : defaultValidationError;
this.option({
isValid: isValid,
validationError: validationError
})
}
_updateHiddenElement() {
this._removeHiddenElement();
const {
mask: mask
} = this.option();
if (mask) {
this._input().removeAttr("name");
this._renderHiddenElement()
}
const {
name: name
} = this.option();
this._setSubmitElementName(name)
}
_updateMaskOption() {
this._updateHiddenElement();
this._renderMask();
this._validateMask();
this._refreshValueChangeEvent()
}
_processEmptyMask(mask) {
if (mask) {
return
}
const {
value: value
} = this.option();
this.option({
text: value,
isValid: true,
validationError: null
});
this.validationRequest.fire({
value: value,
editor: this
});
this._renderValue()
}
_optionChanged(args) {
switch (args.name) {
case "mask":
this._updateMaskOption();
this._processEmptyMask(args.value);
break;
case "maskChar":
case "maskRules":
case "useMaskedValue":
this._updateMaskOption();
break;
case "value":
this._renderMaskedValue();
this._validateMask();
super._optionChanged(args);
this._changedValue = this._input().val();
break;
case "maskInvalidMessage":
break;
case "showMaskMode":
this.option({
text: ""
});
this._renderValue();
break;
default:
super._optionChanged(args)
}
}
clear() {
const {
value: defaultValue
} = this._getDefaultOptions();
const {
value: value
} = this.option();
if (value === defaultValue) {
this._renderMaskedValue()
}
super.clear()
}
}
export default TextEditorMask;