UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

732 lines (731 loc) • 26.5 kB
/** * DevExtreme (esm/__internal/ui/date_box/date_box.base.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 dateLocalization from "../../../common/core/localization/date"; import messageLocalization from "../../../common/core/localization/message"; import config from "../../../core/config"; import devices from "../../../core/devices"; import browser from "../../../core/utils/browser"; import dateUtils from "../../../core/utils/date"; import dateSerialization from "../../../core/utils/date_serialization"; import { createTextElementHiddenCopy } from "../../../core/utils/dom"; import { extend } from "../../../core/utils/extend"; import { inputType } from "../../../core/utils/support"; import { isDate as isDateType, isNumeric, isString } from "../../../core/utils/type"; import { getWindow, hasWindow } from "../../../core/utils/window"; import DropDownEditor from "../../ui/drop_down_editor/m_drop_down_editor"; import uiDateUtils from "./date_utils"; import Calendar from "./m_date_box.strategy.calendar"; import CalendarWithTime from "./m_date_box.strategy.calendar_with_time"; import DateView from "./m_date_box.strategy.date_view"; import List from "./m_date_box.strategy.list"; import Native from "./m_date_box.strategy.native"; const window = getWindow(); const DATEBOX_CLASS = "dx-datebox"; const DX_AUTO_WIDTH_CLASS = "dx-auto-width"; const DX_INVALID_BADGE_CLASS = "dx-show-invalid-badge"; const DX_CLEAR_BUTTON_CLASS = "dx-clear-button-area"; const DATEBOX_WRAPPER_CLASS = "dx-datebox-wrapper"; const DROPDOWNEDITOR_OVERLAY_CLASS = "dx-dropdowneditor-overlay"; const PICKER_TYPE = { calendar: "calendar", rollers: "rollers", list: "list", native: "native" }; const TYPE = { date: "date", datetime: "datetime", time: "time" }; const STRATEGY_NAME = { calendar: "Calendar", dateView: "DateView", native: "Native", calendarWithTime: "CalendarWithTime", list: "List" }; const STRATEGY_CLASSES = { Calendar: Calendar, DateView: DateView, Native: Native, CalendarWithTime: CalendarWithTime, List: List }; class DateBox extends DropDownEditor { _supportedKeys() { return Object.assign({}, super._supportedKeys(), this._strategy.supportedKeys()) } _renderButtonContainers() { super._renderButtonContainers(); this._strategy.customizeButtons() } _getDefaultOptions() { return Object.assign({}, super._getDefaultOptions(), { type: "date", showAnalogClock: true, value: null, displayFormat: null, interval: 30, disabledDates: null, pickerType: PICKER_TYPE.calendar, invalidDateMessage: messageLocalization.format("dxDateBox-validation-datetime"), dateOutOfRangeMessage: messageLocalization.format("validation-range"), applyButtonText: messageLocalization.format("OK"), adaptivityEnabled: false, calendarOptions: {}, useHiddenSubmitElement: true, _showValidationIcon: true }) } _defaultOptionsRules() { return super._defaultOptionsRules().concat([{ device: { platform: "ios" }, options: { "dropDownOptions.showTitle": true } }, { device: { platform: "android" }, options: { buttonsLocation: "bottom after" } }, { device() { const realDevice = devices.real(); const { platform: platform } = realDevice; return "ios" === platform || "android" === platform }, options: { pickerType: PICKER_TYPE.native } }, { device: { platform: "generic", deviceType: "desktop" }, options: { buttonsLocation: "bottom after" } }]) } _initOptions(options) { this._userOptions = extend({}, options); super._initOptions(options); this._updatePickerOptions() } _updatePickerOptions() { let { pickerType: pickerType } = this.option(); const { type: type } = this.option(); if (pickerType === PICKER_TYPE.list && (type === TYPE.datetime || type === TYPE.date)) { pickerType = PICKER_TYPE.calendar } if (type === TYPE.time && pickerType === PICKER_TYPE.calendar) { pickerType = PICKER_TYPE.list } this._pickerType = pickerType; this._setShowDropDownButtonOption() } _setShowDropDownButtonOption() { const { platform: platform } = devices.real(); const isMozillaOnAndroid = "android" === platform && browser.mozilla; const isNativePickerType = this._isNativeType(); let showDropDownButton = "generic" !== platform || !isNativePickerType; if (isNativePickerType && isMozillaOnAndroid) { showDropDownButton = false } this.option({ showDropDownButton: showDropDownButton }) } _init() { this._initStrategy(); this.option(extend({}, this._strategy.getDefaultOptions(), this._userOptions)); delete this._userOptions; super._init() } _toLowerCaseFirstLetter(string) { return string.charAt(0).toLowerCase() + string.substr(1) } _initStrategy() { var _this$_strategy; const strategyName = this._getStrategyName(this._getFormatType()); const strategy = STRATEGY_CLASSES[strategyName]; if (!((null === (_this$_strategy = this._strategy) || void 0 === _this$_strategy ? void 0 : _this$_strategy.NAME) === strategyName)) { this._strategy = new strategy(this) } } _getFormatType() { const { type: type = "date" } = this.option(); const isTime = /h|m|s/g.test(type); const isDate = /d|M|Y/g.test(type); if (isDate && isTime) { return TYPE.datetime } if (isTime) { return TYPE.time } return TYPE.date } _getStrategyName(type) { const pickerType = this._pickerType; if (pickerType === PICKER_TYPE.rollers) { return STRATEGY_NAME.dateView } if (pickerType === PICKER_TYPE.native) { return STRATEGY_NAME.native } if (type === TYPE.date) { return STRATEGY_NAME.calendar } if (type === TYPE.datetime) { return STRATEGY_NAME.calendarWithTime } return STRATEGY_NAME.list } _initMarkup() { this.$element().addClass("dx-datebox"); super._initMarkup(); this._refreshFormatClass(); this._refreshPickerTypeClass(); this._strategy.renderInputMinMax(this._input()) } _render() { super._render(); this._formatValidationIcon() } _renderDimensions() { super._renderDimensions(); const { width: width } = this.option(); this.$element().toggleClass("dx-auto-width", !width); this._updatePopupWidth(); this._updatePopupHeight() } _dimensionChanged() { super._dimensionChanged(); this._updatePopupHeight() } _updatePopupHeight() { if (this._popup && this._strategy instanceof List) { this._strategy._updatePopupHeight() } } _refreshFormatClass() { const $element = this.$element(); const types = Object.values(TYPE); types.forEach(item => { $element.removeClass(`dx-datebox-${item}`) }); const { type: type } = this.option(); $element.addClass(`dx-datebox-${type}`) } _refreshPickerTypeClass() { const $element = this.$element(); const pickerTypes = Object.values(PICKER_TYPE); pickerTypes.forEach(item => { $element.removeClass(`dx-datebox-${item}`) }); $element.addClass(`dx-datebox-${this._pickerType}`) } _formatValidationIcon() { if (!hasWindow()) { return } const inputElement = this._input().get(0); const { rtlEnabled: rtlEnabled } = this.option(); const clearButtonWidth = this._getClearButtonWidth(); const longestElementDimensions = this._getLongestElementDimensions(); const curWidth = parseFloat(window.getComputedStyle(inputElement).width) - clearButtonWidth; const shouldHideValidationIcon = longestElementDimensions.width > curWidth; const { style: style } = inputElement; const { _showValidationIcon: showValidationIcon } = this.option(); this.$element().toggleClass(DX_INVALID_BADGE_CLASS, !shouldHideValidationIcon && showValidationIcon); if (shouldHideValidationIcon) { this._storedPadding ?? (this._storedPadding = rtlEnabled ? longestElementDimensions.leftPadding : longestElementDimensions.rightPadding); if (rtlEnabled) { style.paddingLeft = "0" } else { style.paddingRight = "0" } } else if (rtlEnabled) { style.paddingLeft = `${this._storedPadding}px` } else { style.paddingRight = `${this._storedPadding}px` } } _getClearButtonWidth() { let clearButtonWidth = 0; const input = this._input().get(0); if (this._isClearButtonVisible() && "" === input.value) { const clearButtonElement = this.$element().find(`.${DX_CLEAR_BUTTON_CLASS}`).get(0); clearButtonWidth = parseFloat(window.getComputedStyle(clearButtonElement).width) } return clearButtonWidth } _getLongestElementDimensions() { const { displayFormat: displayFormat } = this.option(); const format = this._strategy.getDisplayFormat(displayFormat); const longestValue = dateLocalization.format(uiDateUtils.getLongestDate(format, dateLocalization.getMonthNames(), dateLocalization.getDayNames()), format); const $input = this._input(); const inputElement = $input.get(0); const $longestValueElement = createTextElementHiddenCopy($input, longestValue); const storedPadding = this._storedPadding ?? 0; $longestValueElement.appendTo(this.$element()); const elementWidth = parseFloat(window.getComputedStyle($longestValueElement.get(0)).width); const rightPadding = parseFloat(window.getComputedStyle(inputElement).paddingRight); const leftPadding = parseFloat(window.getComputedStyle(inputElement).paddingLeft); const necessaryWidth = elementWidth + leftPadding + rightPadding + storedPadding; $longestValueElement.remove(); return { width: necessaryWidth, leftPadding: leftPadding, rightPadding: rightPadding } } _getKeyboardListeners() { var _this$_strategy2; return super._getKeyboardListeners().concat([null === (_this$_strategy2 = this._strategy) || void 0 === _this$_strategy2 ? void 0 : _this$_strategy2.getKeyboardListener()]) } _renderPopup() { var _this$_popup; super._renderPopup(); null === (_this$_popup = this._popup) || void 0 === _this$_popup || null === (_this$_popup = _this$_popup.$wrapper()) || void 0 === _this$_popup || _this$_popup.addClass("dx-datebox-wrapper"); this._renderPopupWrapper() } _getPopupToolbarItems() { const defaultItems = super._getPopupToolbarItems(); return this._strategy._getPopupToolbarItems(defaultItems) } _popupConfig() { const popupConfig = super._popupConfig(); return Object.assign({}, this._strategy.popupConfig(popupConfig), { title: this._getPopupTitle(), dragEnabled: false }) } _renderPopupWrapper() { var _this$_popup$$wrapper; if (!this._popup) { return } const $element = this.$element(); const classPostfixes = [...Object.values(TYPE), ...Object.values(PICKER_TYPE)]; classPostfixes.forEach(item => { $element.removeClass(`dx-datebox-wrapper-${item}`) }); const { type: type } = this.option(); null === (_this$_popup$$wrapper = this._popup.$wrapper()) || void 0 === _this$_popup$$wrapper || _this$_popup$$wrapper.addClass(`dx-datebox-wrapper-${type}`).addClass(`dx-datebox-wrapper-${this._pickerType}`).addClass("dx-dropdowneditor-overlay") } _renderPopupContent() { super._renderPopupContent(); this._strategy.renderPopupContent() } _popupShowingHandler() { super._popupShowingHandler(); this._strategy.popupShowingHandler() } _popupShownHandler() { super._popupShownHandler(); this._strategy.renderOpenedState() } _popupHiddenHandler() { super._popupHiddenHandler(); this._strategy.renderOpenedState(); this._strategy.popupHiddenHandler() } _visibilityChanged(visible) { if (visible) { this._formatValidationIcon() } } _clearValueHandler(e) { this.option("text", ""); super._clearValueHandler(e) } _readOnlyPropValue() { if (this._pickerType === PICKER_TYPE.rollers) { return true } const { platform: platform } = devices.real(); const isCustomValueDisabled = this._isNativeType() && ("ios" === platform || "android" === platform); if (isCustomValueDisabled) { const { readOnly: readOnly = false } = this.option(); return readOnly } return super._readOnlyPropValue() } _isClearButtonVisible() { return super._isClearButtonVisible() && !this._isNativeType() } _renderValue() { const value = this.getDateOption("value"); this.option("text", this._getDisplayedText(value)); this._strategy.renderValue(); return super._renderValue() } _setSubmitValue() { const value = this.getDateOption("value"); const { type: type = "date", dateSerializationFormat: dateSerializationFormat } = this.option(); const submitFormat = uiDateUtils.SUBMIT_FORMATS_MAP[type]; const submitValue = dateSerializationFormat ? dateSerialization.serializeDate(value, dateSerializationFormat) : uiDateUtils.toStandardDateFormat(value, submitFormat); this._getSubmitElement().val(submitValue) } _getDisplayedText(value) { const { mode: mode = "text", displayFormat: displayFormatOption } = this.option(); if ("text" === mode) { const displayFormat = this._strategy.getDisplayFormat(displayFormatOption); return dateLocalization.format(value, displayFormat) } const format = this._getFormatByMode(mode); if (format) { return dateLocalization.format(value, format) } return uiDateUtils.toStandardDateFormat(value, mode) } _getFormatByMode(mode) { return inputType(mode) ? null : uiDateUtils.FORMATS_MAP[mode] } _valueChangeEventHandler(e) { const { text: text, type: type = "date", validationError: validationError } = this.option(); const currentValue = this.getDateOption("value"); if (text === this._getDisplayedText(currentValue)) { this._recallInternalValidation(currentValue, validationError); return } const parsedDate = this._getParsedDate(text); const value = currentValue ?? this._getDateByDefault(); const newValue = uiDateUtils.mergeDates(value, parsedDate, type); const date = parsedDate && "time" === type ? newValue : parsedDate; if (this._applyInternalValidation(date).isValid) { const displayedText = this._getDisplayedText(newValue); if (value && value.getTime() === (null === newValue || void 0 === newValue ? void 0 : newValue.getTime()) && displayedText !== text) { this._renderValue() } else { this.dateValue(newValue, e) } } } _recallInternalValidation(value, validationError) { if (!validationError || validationError.editorSpecific) { this._applyInternalValidation(value); this._applyCustomValidation(value) } } _getDateByDefault() { if (this._strategy.useCurrentDateByDefault()) { return this._strategy.getDefaultDate() } return } _getParsedDate(text) { const { displayFormat: displayFormat } = this.option(); const strategyDisplayFormat = this._strategy.getDisplayFormat(displayFormat); const parsedText = this._strategy.getParsedText(text, strategyDisplayFormat); return parsedText ?? void 0 } _applyInternalValidation(value) { const { text: text, type: type } = this.option(); const hasText = !!text && null !== value; const isDate = !!value && isDateType(value) && !isNaN(value.getTime()); const isDateInRange = isDate && dateUtils.dateInRange(value, this.getDateOption("min"), this.getDateOption("max"), type); const isValid = !hasText && !value || isDateInRange; let validationMessage = ""; const { invalidDateMessage: invalidDateMessage = "", dateOutOfRangeMessage: dateOutOfRangeMessage = "" } = this.option(); if (!isDate) { validationMessage = invalidDateMessage } else if (!isDateInRange) { validationMessage = dateOutOfRangeMessage } this._updateInternalValidationState(isValid, validationMessage); return { isValid: isValid, isDate: isDate } } _updateInternalValidationState(isValid, validationMessage) { this.option({ isValid: isValid, validationError: isValid ? null : { editorSpecific: true, message: validationMessage } }) } _applyCustomValidation(value) { this.validationRequest.fire({ editor: this, value: this._serializeDate(value) }) } _isValueChanged(newValue) { const oldValue = this.getDateOption("value"); const oldTime = oldValue && oldValue.getTime(); const newTime = newValue && newValue.getTime(); return oldTime !== newTime } _isTextChanged(newValue) { const { text: oldText } = this.option(); const newText = (newValue && this._getDisplayedText(newValue)) ?? ""; return oldText !== newText } _renderProps() { super._renderProps(); this._input().attr("autocomplete", "off") } _renderOpenedState() { if (!this._isNativeType()) { super._renderOpenedState() } if (this._strategy.isAdaptivityChanged()) { this._refreshStrategy() } } _getPopupTitle() { const { placeholder: placeholder } = this.option(); if (placeholder) { return placeholder } const { type: type } = this.option(); if (type === TYPE.time) { return messageLocalization.format("dxDateBox-simulatedDataPickerTitleTime") } if (type === TYPE.date || type === TYPE.datetime) { return messageLocalization.format("dxDateBox-simulatedDataPickerTitleDate") } return "" } _refreshStrategy() { this._strategy.dispose(); this._initStrategy(); this.option(this._strategy.getDefaultOptions()); this._refresh() } _applyButtonHandler(e) { const value = this._strategy.getValue(); this.dateValue(value, e.event); super._applyButtonHandler() } _dispose() { var _this$_strategy3; super._dispose(); null === (_this$_strategy3 = this._strategy) || void 0 === _this$_strategy3 || _this$_strategy3.dispose() } _isNativeType() { return this._pickerType === PICKER_TYPE.native } _updatePopupTitle() { var _this$_popup2; null === (_this$_popup2 = this._popup) || void 0 === _this$_popup2 || _this$_popup2.option("title", this._getPopupTitle()) } _optionChanged(args) { switch (args.name) { case "showClearButton": case "buttons": case "isValid": case "readOnly": super._optionChanged(args); this._formatValidationIcon(); break; case "pickerType": this._updatePickerOptions(); this._refreshStrategy(); this._refreshPickerTypeClass(); this._invalidate(); break; case "type": this._updatePickerOptions(); this._refreshStrategy(); this._refreshFormatClass(); this._renderPopupWrapper(); this._formatValidationIcon(); this._updateValue(); break; case "placeholder": super._optionChanged(args); this._updatePopupTitle(); break; case "min": case "max": { const isValid = this.option("isValid"); this._applyInternalValidation(this.getDateOption("value")); if (!isValid) { this._applyCustomValidation(this.getDateOption("value")) } this._invalidate(); break } case "dateSerializationFormat": case "interval": case "disabledDates": case "calendarOptions": case "todayButtonText": this._invalidate(); break; case "displayFormat": this.option("text", this._getDisplayedText(this.getDateOption("value"))); this._renderInputValue(); break; case "text": this._strategy.textChangedHandler(); super._optionChanged(args); break; case "showDropDownButton": this._formatValidationIcon(); super._optionChanged(args); break; case "invalidDateMessage": case "dateOutOfRangeMessage": case "adaptivityEnabled": case "showAnalogClock": case "_showValidationIcon": break; default: super._optionChanged(args) } } _getSerializationFormat() { const { value: value, dateSerializationFormat: dateSerializationFormat } = this.option(); if (dateSerializationFormat && config().forceIsoDateParsing) { return dateSerializationFormat } if (isNumeric(value)) { return "number" } if (!isString(value) || "" === value) { return } return dateSerialization.getDateSerializationFormat(value) } _updateValue(value) { super._updateValue(); this._applyInternalValidation(value ?? this.getDateOption("value")) } dateValue(value, dxEvent) { const isValueChanged = this._isValueChanged(value); if (isValueChanged && dxEvent) { this._saveValueChangeEvent(dxEvent) } if (!isValueChanged) { const { text: text } = this.option(); if (this._isTextChanged(value)) { this._updateValue(value) } else if ("" === text) { this._applyCustomValidation(value) } } this.setDateOption("value", value) } getDateOption(optionName) { const { [optionName]: optionValue } = this.option(); const deserializedDate = dateSerialization.deserializeDate(optionValue); return deserializedDate } setDateOption(optionName, value) { const serializedDate = this._serializeDate(value); this.option(optionName, serializedDate) } _serializeDate(date) { const serializationFormat = this._getSerializationFormat(); const serializedDate = dateSerialization.serializeDate(date, serializationFormat); return serializedDate } _clearValue() { const { value: value } = this.option(); super._clearValue(); if (null === value) { this._applyCustomValidation(null) } } clear() { const { value: value } = this.option(); super.clear(); if (null === value) { this._applyInternalValidation(null) } } } export default DateBox;