UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

698 lines (697 loc) • 26.2 kB
/** * DevExtreme (esm/__internal/ui/date_box/date_box.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 { addNamespace, isCommandKeyPressed, normalizeKeyName } from "../../../common/core/events/utils/index"; import { getFormat } from "../../../common/core/localization/ldml/date.format"; import { getRegExpInfo } from "../../../common/core/localization/ldml/date.parser"; import numberLocalization from "../../../common/core/localization/number"; import devices from "../../../core/devices"; import browser from "../../../core/utils/browser"; import { clipboardText } from "../../../core/utils/dom"; import { fitIntoRange, inRange, sign } from "../../../core/utils/math"; import { isDate, isDefined, isFunction, isString } from "../../../core/utils/type"; import dateLocalization from "../../core/localization/date"; import DateBoxBase from "./date_box.base"; import { getDatePartIndexByPosition, renderDateParts } from "./date_box.mask.parts"; const MASK_EVENT_NAMESPACE = "dateBoxMask"; const FORWARD = 1; const BACKWARD = -1; class DateBoxMask extends DateBoxBase { _supportedKeys() { const originalHandlers = super._supportedKeys(); const callOriginalHandler = e => { const normalizedKeyName = normalizeKeyName(e); const originalHandler = normalizedKeyName ? originalHandlers[normalizedKeyName] : void 0; return null === originalHandler || void 0 === originalHandler ? void 0 : originalHandler.apply(this, [e]) }; const applyHandler = (e, maskHandler) => { if (this._shouldUseOriginalHandler(e)) { return callOriginalHandler.apply(this, [e]) } return maskHandler.apply(this, [e]) }; return Object.assign({}, originalHandlers, { del: e => applyHandler(e, event => { this._revertPart(1); if (!this._isAllSelected()) { event.preventDefault() } }), backspace: e => applyHandler(e, event => { this._revertPart(-1); if (!this._isAllSelected()) { event.preventDefault() } }), home: e => applyHandler(e, event => { this._selectFirstPart(); event.preventDefault() }), end: e => applyHandler(e, event => { this._selectLastPart(); event.preventDefault() }), escape: e => applyHandler(e, () => { this._revertChanges() }), enter: e => applyHandler(e, () => { this._enterHandler() }), leftArrow: e => applyHandler(e, event => { this._selectNextPart(-1); event.preventDefault() }), rightArrow: e => applyHandler(e, event => { this._selectNextPart(1); event.preventDefault() }), upArrow: e => applyHandler(e, event => { this._upDownArrowHandler(1); event.preventDefault() }), downArrow: e => applyHandler(e, event => { this._upDownArrowHandler(-1); event.preventDefault() }) }) } _shouldUseOriginalHandler(e) { const { opened: opened = false } = this.option(); const isNotDeletingInCalendar = opened && e && !["backspace", "del"].includes(normalizeKeyName(e) ?? ""); return !this._useMaskBehavior() || isNotDeletingInCalendar || (null === e || void 0 === e ? void 0 : e.altKey) } _upDownArrowHandler(step) { this._setNewDateIfEmpty(); const originalValue = this._getActivePartValue(this._initialMaskValue); const currentValue = this._getActivePartValue(); const delta = currentValue - originalValue; this._loadMaskValue(this._initialMaskValue); this._changePartValue(delta + step, true) } _changePartValue(step, lockOtherParts) { const activePartPattern = this._getActivePartProp("pattern"); const isAmPmPartActive = /^a{1,5}$/.test(activePartPattern); if (isAmPmPartActive) { this._toggleAmPm() } else { this._partIncrease(step, lockOtherParts) } } _toggleAmPm() { const currentValue = this._getActivePartProp("text"); const periodNames = dateLocalization.getPeriodNames(this._formatPattern); const indexOfCurrentValue = periodNames.indexOf(currentValue); const newValue = 1 ^ indexOfCurrentValue; this._setActivePartValue(newValue) } _getDefaultOptions() { return Object.assign({}, super._getDefaultOptions(), { useMaskBehavior: false, emptyDateValue: new Date(2e3, 0, 1, 0, 0, 0) }) } _isSingleCharKey(_ref) { let { originalEvent: originalEvent, alt: alt } = _ref; const key = originalEvent.data ?? originalEvent.key; return "string" === typeof key && 1 === key.length && !alt && !isCommandKeyPressed(originalEvent) } _isSingleDigitKey(e) { var _e$originalEvent; const data = null === (_e$originalEvent = e.originalEvent) || void 0 === _e$originalEvent ? void 0 : _e$originalEvent.data; return 1 === (null === data || void 0 === data ? void 0 : data.length) && Boolean(parseInt(data, 10)) } _useBeforeInputEvent() { return Boolean(devices.real().android) } _keyInputHandler(e, key) { const oldInputValue = this._input().val(); this._processInputKey(key); e.preventDefault(); const isValueChanged = oldInputValue !== this._input().val(); if (isValueChanged) { eventsEngine.triggerHandler(this._input(), { type: "input" }) } } _keyboardHandler(e) { let { key: key } = e.originalEvent; const result = super._keyboardHandler(e); if (!this._useMaskBehavior() || this._useBeforeInputEvent()) { return result } if (browser.chrome && "Process" === e.key && e.code.startsWith("Digit")) { key = e.code.replace("Digit", ""); this._processInputKey(key); this._maskInputHandler = () => { this._renderSelectedPart() } } else if (this._isSingleCharKey(e)) { this._keyInputHandler(e.originalEvent, key) } return result } _maskBeforeInputHandler(e) { this._maskInputHandler = null; const { inputType: inputType } = e.originalEvent; if ("insertCompositionText" === inputType) { this._maskInputHandler = () => { this._renderSelectedPart() } } const isBackwardDeletion = "deleteContentBackward" === inputType; const isForwardDeletion = "deleteContentForward" === inputType; if (isBackwardDeletion || isForwardDeletion) { const direction = isBackwardDeletion ? -1 : 1; this._maskInputHandler = () => { this._revertPart(); this._selectNextPart(direction) } } if (!this._useMaskBehavior() || !this._isSingleCharKey(e)) { return false } const key = e.originalEvent.data ?? ""; this._keyInputHandler(e, key); return true } _keyPressHandler(e) { const { originalEvent: event } = e; if ("insertCompositionText" === (null === event || void 0 === event ? void 0 : event.inputType) && this._isSingleDigitKey(e)) { this._processInputKey(event.data ?? ""); this._renderDisplayText(this._getDisplayedText(this._maskValue)); this._selectNextPart() } super._keyPressHandler(e); if (this._maskInputHandler) { this._maskInputHandler(); this._maskInputHandler = null } } _processInputKey(key) { var _this$_dateParts; const hasMultipleParts = (null === (_this$_dateParts = this._dateParts) || void 0 === _this$_dateParts ? void 0 : _this$_dateParts.length) > 1; if (this._isAllSelected() && hasMultipleParts) { this._activePartIndex = 0; this._clearSearchValue() } this._setNewDateIfEmpty(); if (isNaN(parseInt(key, 10))) { this._searchString(key) } else { this._searchNumber(key) } } _isAllSelected() { const caret = this._caret(); const { text: text = "" } = this.option(); const caretStart = (null === caret || void 0 === caret ? void 0 : caret.start) ?? 0; const caretEnd = (null === caret || void 0 === caret ? void 0 : caret.end) ?? 0; return caretEnd - caretStart === text.length } _getFormatPattern() { if (this._formatPattern) { return this._formatPattern } const { displayFormat: displayFormat } = this.option(); const format = this._strategy.getDisplayFormat(displayFormat); const isLDMLPattern = isString(format) && !dateLocalization._getPatternByFormat(format); if (isLDMLPattern) { this._formatPattern = format } else { this._formatPattern = getFormat(value => dateLocalization.format(value, format)) } return this._formatPattern } _setNewDateIfEmpty() { if (!this._maskValue) { const { type: type } = this.option(); const value = "time" === type ? new Date(0) : new Date; this._maskValue = value; this._initialMaskValue = value; this._renderDateParts() } } _partLimitsReached(max) { const maxLimitLength = String(max).length; const formatLength = this._getActivePartProp("pattern").length; const isShortFormat = 1 === formatLength; const maxSearchLength = isShortFormat ? maxLimitLength : Math.min(formatLength, maxLimitLength); const isLengthExceeded = this._searchValue.length === maxSearchLength; const isValueOverflowed = parseInt(`${this._searchValue}0`, 10) > max; return isLengthExceeded || isValueOverflowed } _searchNumber(char) { const { max: max } = this._getActivePartLimits(); const maxLimitLength = String(max).length; this._searchValue = (this._searchValue + char).substr(-maxLimitLength); if (isNaN(parseInt(this._searchValue, 10))) { this._searchValue = char } this._setActivePartValue(this._searchValue); if (this._partLimitsReached(max)) { this._selectNextPart(1) } } _searchString(char) { const text = this._getActivePartProp("text"); const convertedText = numberLocalization.convertDigits(text, true); if (!isNaN(parseInt(convertedText, 10))) { return } const limits = this._getActivePartProp("limits")(this._maskValue); const startString = this._searchValue + char.toLowerCase(); const endLimit = limits.max - limits.min; for (let i = 0; i <= endLimit; i += 1) { this._loadMaskValue(this._initialMaskValue); this._changePartValue(i + 1); if (this._getActivePartProp("text").toLowerCase().startsWith(startString)) { this._searchValue = startString; return } } this._setNewDateIfEmpty(); if (this._searchValue) { this._clearSearchValue(); this._searchString(char) } } _clearSearchValue() { this._searchValue = "" } _revertPart(direction) { if (!this._isAllSelected()) { const { emptyDateValue: emptyDateValue } = this.option(); const actual = this._getActivePartValue(emptyDateValue); this._setActivePartValue(actual); this._selectNextPart(direction) } this._clearSearchValue() } _useMaskBehavior() { const { mode: mode } = this.option(); return this.option("useMaskBehavior") && "text" === mode } _prepareRegExpInfo() { this._regExpInfo = getRegExpInfo(this._getFormatPattern(), dateLocalization); const { regexp: regexp } = this._regExpInfo; const { source: source } = regexp; const { flags: flags } = regexp; const quantifierRegexp = new RegExp(/(\{[0-9]+,?[0-9]*\})/); const convertedSource = source.split(quantifierRegexp).map(sourcePart => quantifierRegexp.test(sourcePart) ? sourcePart : numberLocalization.convertDigits(sourcePart, false)).join(""); this._regExpInfo.regexp = new RegExp(convertedSource, flags) } _initMaskState() { this._activePartIndex = 0; this._formatPattern = null; this._prepareRegExpInfo(); this._loadMaskValue() } _renderMask() { super._renderMask(); this._detachMaskEvents(); this._clearMaskState(); if (this._useMaskBehavior()) { this._attachMaskEvents(); this._initMaskState(); this._renderDateParts() } } _renderDateParts() { if (!this._useMaskBehavior()) { return } const { text: text } = this.option(); const newText = text || this._getDisplayedText(this._maskValue); if (newText) { this._dateParts = renderDateParts(newText, this._regExpInfo); if (!this._input().is(":hidden")) { this._selectNextPart() } } } _detachMaskEvents() { eventsEngine.off(this._input(), ".dateBoxMask") } _attachMaskEvents() { eventsEngine.on(this._input(), addNamespace("dxclick", "dateBoxMask"), this._maskClickHandler.bind(this)); eventsEngine.on(this._input(), addNamespace("paste", "dateBoxMask"), this._maskPasteHandler.bind(this)); eventsEngine.on(this._input(), addNamespace("drop", "dateBoxMask"), () => { this._renderSelectedPart() }); eventsEngine.on(this._input(), addNamespace("compositionend", "dateBoxMask"), this._maskCompositionEndHandler.bind(this)); if (this._useBeforeInputEvent()) { eventsEngine.on(this._input(), addNamespace("beforeinput", "dateBoxMask"), this._maskBeforeInputHandler.bind(this)) } } _renderSelectedPart() { this._renderDisplayText(this._getDisplayedText(this._maskValue)); this._selectNextPart() } _selectLastPart() { if (this.option("text")) { this._activePartIndex = this._dateParts.length; this._selectNextPart(-1) } } _selectFirstPart() { if (this.option("text") && this._dateParts) { this._activePartIndex = -1; this._selectNextPart(1) } } _hasMouseWheelHandler() { return true } _onMouseWheel(e) { if (this._useMaskBehavior()) { this._partIncrease(e.delta > 0 ? 1 : -1, Boolean(e)) } } _selectNextPart() { var _this$_dateParts$inde; let step = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0; if (!this.option("text") || this._disposed) { return } if (step) { this._initialMaskValue = new Date(this._maskValue) } const activePartIndex = this._activePartIndex ?? 0; let index = fitIntoRange(activePartIndex + step, 0, this._dateParts.length - 1); if (null !== (_this$_dateParts$inde = this._dateParts[index]) && void 0 !== _this$_dateParts$inde && _this$_dateParts$inde.isStub) { const isBoundaryIndex = 0 === index && step < 0 || index === this._dateParts.length - 1 && step > 0; if (!isBoundaryIndex) { this._selectNextPart(step >= 0 ? step + 1 : step - 1); return } index = activePartIndex } if (activePartIndex !== index) { this._clearSearchValue() } this._activePartIndex = index; this._caret(this._getActivePartProp("caret")) } _getRealLimitsPattern() { if (this._getActivePartProp("pattern").startsWith("d")) { return "dM" } return } _getActivePartLimits() { let lockOtherParts = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : false; const limitFunction = this._getActivePartProp("limits"); return limitFunction(this._maskValue, lockOtherParts ? this._getRealLimitsPattern() : void 0) } _getActivePartValue(dateValue) { const date = dateValue ?? this._maskValue; const getter = this._getActivePartProp("getter"); const isGetterFunction = isFunction(getter); const activePartValue = isGetterFunction ? getter(date) : date[getter](); return activePartValue } _addLeadingZeroes(value) { const zeroes = /^0+/.exec(this._searchValue); const limits = this._getActivePartLimits(); const maxLimitLength = String(limits.max).length; return (((null === zeroes || void 0 === zeroes ? void 0 : zeroes[0]) ?? "") + String(value)).substr(-maxLimitLength) } _setActivePartValue(value, dateValue) { let newValue = +value; const newDateValue = dateValue ?? this._maskValue; const setter = this._getActivePartProp("setter"); const limits = this._getActivePartLimits(); newValue = inRange(newValue, limits.min, limits.max) ? newValue : newValue % 10; newValue = this._addLeadingZeroes(fitIntoRange(newValue, limits.min, limits.max)); if (isFunction(setter)) { setter(newDateValue, newValue) } else { newDateValue[setter](newValue) } this._renderDisplayText(this._getDisplayedText(newDateValue)); this._renderDateParts() } _getActivePartProp(property) { var _this$_dateParts2; if (!isDefined(this._activePartIndex)) { return } if (!(null !== (_this$_dateParts2 = this._dateParts) && void 0 !== _this$_dateParts2 && _this$_dateParts2[this._activePartIndex])) { return } return this._dateParts[this._activePartIndex][property] } _loadMaskValue() { let value = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.getDateOption("value"); this._maskValue = value ? new Date(value) : null; this._initialMaskValue = value ? new Date(value) : null } _saveMaskValue() { const value = this._maskValue && new Date(this._maskValue); const { type: type } = this.option(); if (value && "date" === type) { value.setHours(0, 0, 0, 0) } this._initialMaskValue = new Date(value); if (this._applyInternalValidation(value).isValid) { this.setDateOption("value", value) } } _revertChanges() { this._loadMaskValue(); this._renderDisplayText(this._getDisplayedText(this._maskValue)); this._renderDateParts() } _renderDisplayText(text) { super._renderDisplayText(text); if (this._useMaskBehavior()) { this.option("text", text) } } _partIncrease(step, lockOtherParts) { this._setNewDateIfEmpty(); const { max: max, min: min } = this._getActivePartLimits(lockOtherParts); let newValue = step + this._getActivePartValue(); if (newValue > max) { newValue = this._applyLimits(newValue, { limitBase: min, limitClosest: max, max: max }) } else if (newValue < min) { newValue = this._applyLimits(newValue, { limitBase: max, limitClosest: min, max: max }) } this._setActivePartValue(newValue) } _applyLimits(newValue, _ref2) { let { limitBase: limitBase, limitClosest: limitClosest, max: max } = _ref2; const delta = (newValue - limitClosest) % max; return delta ? limitBase + delta - 1 * sign(delta) : limitClosest } _maskClickHandler() { this._loadMaskValue(this._maskValue); const { text: text } = this.option(); if (text) { var _this$_caret; this._activePartIndex = getDatePartIndexByPosition(this._dateParts, (null === (_this$_caret = this._caret()) || void 0 === _this$_caret ? void 0 : _this$_caret.start) ?? 0); if (!this._isAllSelected()) { this._clearSearchValue(); if (isDefined(this._activePartIndex)) { this._caret(this._getActivePartProp("caret")) } else { this._selectLastPart() } } } } _maskCompositionEndHandler() { this._input().val(this._getDisplayedText(this._maskValue)); this._selectNextPart(); this._maskInputHandler = () => { this._renderSelectedPart() } } _maskPasteHandler(e) { const { text: text } = this.option(); const newText = this._replaceSelectedText(text, this._caret(), clipboardText(e)); const date = dateLocalization.parse(newText, this._getFormatPattern()); if (date && this._isDateValid(date)) { this._maskValue = date; this._renderDisplayText(this._getDisplayedText(this._maskValue)); this._renderDateParts(); this._selectNextPart() } e.preventDefault() } _isDateValid(date) { return isDate(date) && !isNaN(date.getTime()) } _isValueDirty() { var _this$_maskValue; const value = this.getDateOption("value"); return (null === (_this$_maskValue = this._maskValue) || void 0 === _this$_maskValue ? void 0 : _this$_maskValue.getTime()) !== (null === value || void 0 === value ? void 0 : value.getTime()) } _hasEditorSpecificValidationError() { const { isValid: isValid, validationError: validationError } = this.option(); return !isValid && Boolean(null === validationError || void 0 === validationError ? void 0 : validationError.editorSpecific) } _fireChangeEvent() { this._clearSearchValue(); if (this._isValueDirty() || this._hasEditorSpecificValidationError()) { eventsEngine.triggerHandler(this._input(), { type: "change" }) } } _enterHandler() { this._fireChangeEvent(); if (this._useMaskBehavior() && this._isAllSelected()) { this._selectFirstPart() } else { this._selectNextPart(1) } } _focusOutHandler(e) { const shouldFireChangeEvent = this._useMaskBehavior() && !e.isDefaultPrevented(); if (shouldFireChangeEvent) { this._fireChangeEvent(); super._focusOutHandler(e) } else { super._focusOutHandler(e) } } _valueChangeEventHandler(e) { const { text: text } = this.option(); if (this._useMaskBehavior()) { this._saveValueChangeEvent(e); if (!text) { this._maskValue = null } else if (null === this._maskValue) { this._loadMaskValue(text) } this._saveMaskValue() } else { super._valueChangeEventHandler(e) } } _optionChanged(args) { switch (args.name) { case "useMaskBehavior": this._renderMask(); break; case "displayFormat": case "mode": super._optionChanged(args); this._renderMask(); break; case "value": this._loadMaskValue(); super._optionChanged(args); this._renderDateParts(); break; case "emptyDateValue": break; default: super._optionChanged(args) } } _clearMaskState() { this._clearSearchValue(); delete this._dateParts; delete this._activePartIndex; delete this._maskValue } clear() { this._clearMaskState(); this._activePartIndex = 0; super.clear() } _clean() { super._clean(); this._detachMaskEvents(); this._clearMaskState() } } export default DateBoxMask;