devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
698 lines (697 loc) • 26.2 kB
JavaScript
/**
* 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;