UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

530 lines (526 loc) • 16.9 kB
/** * DevExtreme (esm/__internal/ui/text_box/m_text_editor.mask.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import _extends from "@babel/runtime/helpers/esm/extends"; 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 { noop } from "../../../core/utils/common"; import { extend } from "../../../core/utils/extend"; import { each } from "../../../core/utils/iterator"; import { isEmpty } from "../../../core/utils/string"; import { isDefined } from "../../../core/utils/type"; import { focused } from "../../../ui/widget/selectors"; import TextEditorBase from "./m_text_editor.base"; import { EmptyMaskRule, MaskRule, StubMaskRule } from "./m_text_editor.mask.rule"; import MaskStrategy from "./m_text_editor.mask.strategy"; import caretUtils from "./m_utils.caret"; const caret = caretUtils; 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 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) }; function isNumericChar(char) { return /[0-9]/.test(char) } function isLiteralChar(char) { const code = char.charCodeAt(); return code > 64 && code < 91 || code > 96 && code < 123 || code > 127 } function isSpaceChar(char) { return " " === char } class TextEditorMask extends TextEditorBase { _getDefaultOptions() { return _extends({}, super._getDefaultOptions(), { mask: "", maskChar: "_", maskRules: {}, maskInvalidMessage: messageLocalization.format("validation-mask"), useMaskedValue: false, showMaskMode: "always" }) } _supportedKeys() { const that = this; const keyHandlerMap = { del: that._maskStrategy.getHandler("del"), enter: that._changeHandler }; const result = super._supportedKeys(); each(keyHandlerMap, ((key, callback) => { const parentHandler = result[key]; result[key] = function(e) { that.option("mask") && callback.call(that, e); parentHandler && parentHandler(e) } })); return result } _getSubmitElement() { return !this.option("mask") ? super._getSubmitElement() : this._$hiddenElement } _init() { super._init(); this._initMaskStrategy() } _initMaskStrategy() { this._maskStrategy = new MaskStrategy(this) } _initMarkup() { this._renderHiddenElement(); super._initMarkup() } _attachMouseWheelEventHandlers() { const hasMouseWheelHandler = this._onMouseWheel !== noop; if (!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 }) })) } _onMouseWheel(e) {} _useMaskBehavior() { return Boolean(this.option("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() { if (this.option("mask")) { this._$hiddenElement = $("<input>").attr("type", "hidden").appendTo(this._inputWrapper()) } } _removeHiddenElement() { this._$hiddenElement && this._$hiddenElement.remove() } _renderMask() { this.$element().removeClass("dx-texteditor-masked"); this._maskRulesChain = null; this._maskStrategy.detachEvents(); if (!this.option("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() { this._maskRules = extend({}, buildInMaskRules, this.option("maskRules")); this._maskRulesChain = this._parseMaskRule(0) } _parseMaskRule(index) { const { mask: mask } = this.option(); if (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); result.next(this._parseMaskRule(index + 1 + isEscapedChar)); return result } _getMaskRule(pattern) { let ruleConfig; each(this._maskRules, ((rulePattern, allowedChars) => { if (rulePattern === pattern) { ruleConfig = { pattern: rulePattern, allowedChars: allowedChars }; return false } })); return isDefined(ruleConfig) ? new MaskRule(extend({ maskChar: this.option("maskChar") || " " }, ruleConfig)) : new StubMaskRule({ maskChar: pattern }) } _renderMaskedValue() { if (!this._maskRulesChain) { return } const value = this.option("value") || ""; this._maskRulesChain.clear(this._normalizeChainArguments()); const chainArgs = { length: value.length }; chainArgs[this._isMaskedValueMode() ? "text" : "value"] = 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() { return this.option("useMaskedValue") } _displayMask(caret) { caret = caret || this._caret(); this._renderValue(); this._caret(caret) } _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()) { const text = 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(e, value) { if (!this._maskRulesChain) { super._valueChangeEventHandler.apply(this, arguments); return } this._saveValueChangeEvent(e); this.option("value", this._getPreparedValue()) } _isControlKeyFired(e) { return this._isControlKey(normalizeKeyName(e)) || isCommandKeyPressed(e) } _handleChain(args) { const handledCount = this._maskRulesChain.handle(this._normalizeChainArguments(args)); this._updateMaskInfo(); return handledCount } _normalizeChainArguments(args) { args = args || {}; args.index = 0; args.fullText = this._maskRulesChain.text(); return args } _convertToValue(text) { if (this._isMaskedValueMode()) { text = this._replaceMaskCharWithEmpty(text || this._textValue || "") } else { text = text || this._value || "" } return text } _replaceMaskCharWithEmpty(text) { const { maskChar: maskChar } = this.option(); return text.replace(new RegExp(maskChar, "g"), " ") } _maskKeyHandler(e, keyHandler) { if (this.option("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 { this.setForwardDirection(); this._adjustCaret(); this._displayMask(); this._maskRulesChain.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 emptyChars = new Array(caret.end - caret.start + 1).join(" "); this._handleKeyChain(emptyChars) } _handleKeyChain(chars) { const caret = this._caret(); const start = this.isForwardDirection() ? caret.start : caret.start - 1; const end = this.isForwardDirection() ? caret.end : caret.end - 1; const length = start === end ? 1 : end - start; this._handleChain({ text: chars, start: start, length: length }) } _tryMoveCaretBackward() { this.setBackwardDirection(); const currentCaret = this._caret().start; this._adjustCaret(); return !currentCaret || currentCaret !== this._caret().start } _adjustCaret(char) { const caretStart = this._caret().start; const isForwardDirection = this.isForwardDirection(); const caret = this._maskRulesChain.adjustedCaret(caretStart, isForwardDirection, char); this._caret({ start: caret, end: caret }) } _moveCaret() { const currentCaret = this._caret().start; const maskRuleIndex = currentCaret + (this.isForwardDirection() ? 0 : -1); const caret = this._maskRulesChain.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) { return caret($input) } caret($input, position, force) } _hasSelection() { const caret = this._caret(); return caret.start !== 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() { this._textValue = this._maskRulesChain.text(); this._value = this._maskRulesChain.value() } _clean() { this._maskStrategy && this._maskStrategy.clean(); super._clean() } _validateMask() { if (!this._maskRulesChain) { return } const isValid = isEmpty(this.option("value")) || this._maskRulesChain.isValid(this._normalizeChainArguments()); this.option({ isValid: isValid, validationError: isValid ? null : { editorSpecific: true, message: this.option("maskInvalidMessage") } }) } _updateHiddenElement() { this._removeHiddenElement(); if (this.option("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 = this.option("value"); 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(); if (this.option("value") === defaultValue) { this._renderMaskedValue() } super.clear() } } export default TextEditorMask;