armtek-uikit-react 1.0.269 → 1.0.270

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ type OwnProps = {
15
15
  value?: Date | string | null;
16
16
  showTime?: boolean;
17
17
  showTimeOnly?: boolean;
18
+ preserveIncompleteTimeInput?: boolean;
18
19
  inputProps?: Omit<TextFieldProps, 'onChange' | 'value'> & DataAttributes;
19
20
  showMonthYearPicker?: boolean;
20
21
  showYearPicker?: boolean;
@@ -11,12 +11,15 @@ const ui_Adornment_Adornment = require("../../Adornment/Adornment.cjs");
11
11
  const ui_Popper_Popper = require("../../Popper/Popper.cjs");
12
12
  require("../../Popper/PopperBase.cjs");
13
13
  const DELIMITER = " - ";
14
+ const COMPLETE_DATE_REGEX = /^\d{2}\.\d{2}\.\d{4}$/;
15
+ const COMPLETE_PERIOD_REGEX = new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`);
14
16
  const format = "dd.MM.yyyy";
15
17
  function getPeriodDisplay(date1, date2) {
16
18
  if (date1 && date2) return lib_services_DateService.DateService(date1).format(format) + DELIMITER + lib_services_DateService.DateService(date2).format(format);
17
19
  return "";
18
20
  }
19
21
  function validateDate(date, minDate, maxDate) {
22
+ if (!COMPLETE_DATE_REGEX.test(date)) return null;
20
23
  const startParts = date.split(".");
21
24
  const startTimeStamp = Date.parse(`${startParts[2]}-${startParts[1]}-${startParts[0]}`);
22
25
  if (isNaN(startTimeStamp) === false) {
@@ -27,6 +30,55 @@ function validateDate(date, minDate, maxDate) {
27
30
  }
28
31
  return null;
29
32
  }
33
+ function parseSerializedDateTuple(value) {
34
+ const boundaryIndex = value.indexOf("),");
35
+ if (boundaryIndex < 0) return null;
36
+ const leftPart = value.slice(0, boundaryIndex + 1).trim();
37
+ const rightPart = value.slice(boundaryIndex + 2).trim();
38
+ const leftDate = new Date(leftPart);
39
+ const rightDate = new Date(rightPart);
40
+ if (Number.isNaN(leftDate.getTime()) || Number.isNaN(rightDate.getTime()) || rightDate.getTime() < leftDate.getTime()) return null;
41
+ return [leftDate, rightDate];
42
+ }
43
+ function isBackspaceAtPeriodBoundary(prevValue, nextValue) {
44
+ return (prevValue.endsWith(".") || prevValue.endsWith(" ") || prevValue.endsWith("-")) && prevValue.length > nextValue.length;
45
+ }
46
+ function normalizePeriodInput(rawValue) {
47
+ return rawValue.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})(\\d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})(\\d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
48
+ }
49
+ function parsePeriodInputForChange(value, minDate, maxDate) {
50
+ const valueParts = value.split(DELIMITER);
51
+ let startDateValid = null;
52
+ let endDateValid = null;
53
+ if (COMPLETE_DATE_REGEX.test(valueParts[0])) {
54
+ startDateValid = validateDate(valueParts[0], minDate, maxDate);
55
+ if (!startDateValid) return {
56
+ type: "invalid-start"
57
+ };
58
+ }
59
+ if (COMPLETE_PERIOD_REGEX.test(value) && valueParts.length === 2) {
60
+ endDateValid = validateDate(valueParts[1], minDate, maxDate);
61
+ if (!endDateValid) return {
62
+ type: "invalid-end"
63
+ };
64
+ }
65
+ if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) return {
66
+ type: "complete",
67
+ value: [startDateValid, endDateValid]
68
+ };
69
+ return {
70
+ type: "partial"
71
+ };
72
+ }
73
+ function parseStrictPeriodValue(value, minDate, maxDate) {
74
+ const valueParts = value.split(DELIMITER);
75
+ if (valueParts.length !== 2) return null;
76
+ const startDateValid = validateDate(valueParts[0], minDate, maxDate);
77
+ const endDateValid = validateDate(valueParts[1], minDate, maxDate);
78
+ if (!startDateValid || !endDateValid) return null;
79
+ if (endDateValid.getTime() < startDateValid.getTime()) return null;
80
+ return [startDateValid, endDateValid];
81
+ }
30
82
  const Period = React.forwardRef((props, ref) => {
31
83
  const {
32
84
  onChange,
@@ -47,11 +99,56 @@ const Period = React.forwardRef((props, ref) => {
47
99
  const displayText = defaultValue ? getPeriodDisplay(defaultValue[0], defaultValue[1]) : "";
48
100
  const [textValue, setTextValue] = React.useState(displayText);
49
101
  const btnRef = React.useRef();
102
+ const inputRef = React.useRef(null);
103
+ const didSyncFromRefValueRef = React.useRef(false);
104
+ const datesEqual = (left, right) => {
105
+ var _a, _b, _c, _d;
106
+ return (((_a = left[0]) == null ? void 0 : _a.getTime()) || 0) === (((_b = right[0]) == null ? void 0 : _b.getTime()) || 0) && (((_c = left[1]) == null ? void 0 : _c.getTime()) || 0) === (((_d = right[1]) == null ? void 0 : _d.getTime()) || 0);
107
+ };
108
+ const setInputRef = (node) => {
109
+ inputRef.current = node;
110
+ if (typeof ref === "function") ref(node);
111
+ else if (ref) ref.current = node;
112
+ if (node && !Array.isArray(props.value) && !didSyncFromRefValueRef.current) {
113
+ syncFromInputValue(node.value);
114
+ didSyncFromRefValueRef.current = true;
115
+ }
116
+ };
50
117
  const [startValue, endValue] = value || [];
51
118
  React.useEffect(() => {
52
119
  if (startValue && endValue) setTextValue(getPeriodDisplay(startValue, endValue));
53
120
  else if (startValue === null && endValue === null) setTextValue("");
54
121
  }, [startValue, endValue]);
122
+ const syncFromInputValue = (nextInputValue) => {
123
+ if (Array.isArray(props.value)) return;
124
+ if (!nextInputValue) {
125
+ setTextValue((prev) => prev === "" ? prev : "");
126
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
127
+ return;
128
+ }
129
+ const valueParts = nextInputValue.split(DELIMITER);
130
+ if (valueParts.length !== 2) {
131
+ const serializedDates = parseSerializedDateTuple(nextInputValue);
132
+ if (!serializedDates) return;
133
+ const normalizedText2 = getPeriodDisplay(serializedDates[0], serializedDates[1]);
134
+ setCurrentValue((prev) => datesEqual(prev, serializedDates) ? prev : serializedDates);
135
+ setTextValue((prev) => prev === normalizedText2 ? prev : normalizedText2);
136
+ return;
137
+ }
138
+ const startDate = validateDate(valueParts[0], minDate, maxDate);
139
+ const endDate = validateDate(valueParts[1], minDate, maxDate);
140
+ if (!startDate || !endDate || endDate.getTime() < startDate.getTime()) {
141
+ setTextValue((prev) => prev === nextInputValue ? prev : nextInputValue);
142
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
143
+ return;
144
+ }
145
+ const normalizedText = getPeriodDisplay(startDate, endDate);
146
+ setCurrentValue((prev) => {
147
+ const nextPeriod = [startDate, endDate];
148
+ return datesEqual(prev, nextPeriod) ? prev : nextPeriod;
149
+ });
150
+ setTextValue((prev) => prev === normalizedText ? prev : normalizedText);
151
+ };
55
152
  const handleClick = (e) => {
56
153
  e.stopPropagation();
57
154
  setActive((prev) => !prev);
@@ -81,43 +178,26 @@ const Period = React.forwardRef((props, ref) => {
81
178
  };
82
179
  const handleChange = (e) => {
83
180
  if (disabled) return;
84
- let value2 = e.target.value;
85
- const bs = (textValue.endsWith(".") || textValue.endsWith(" ") || textValue.endsWith("-")) && textValue.length > value2.length;
86
- if (!bs) {
87
- value2 = value2.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})(d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})(d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
88
- }
89
- let startDateValid = null;
90
- let endDateValid = null;
91
- const valueParts = value2.split(DELIMITER);
92
- if (valueParts[0].match(/^\d{2}\.\d{2}\.\d{4}$/)) {
93
- startDateValid = validateDate(valueParts[0], minDate, maxDate);
94
- if (!startDateValid) {
95
- setTextValue("");
96
- handleSelect([null, null]);
97
- return;
98
- }
99
- }
181
+ const rawValue = e.target.value;
182
+ const value2 = isBackspaceAtPeriodBoundary(textValue, rawValue) ? rawValue : normalizePeriodInput(rawValue);
100
183
  setTextValue(value2);
101
184
  setCurrentValue([null, null]);
102
- if (value2.match(new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`))) {
103
- if (valueParts.length === 2) {
104
- endDateValid = validateDate(valueParts[1], minDate, maxDate);
105
- if (!endDateValid) {
106
- setTextValue("");
107
- }
108
- }
185
+ const parseResult = parsePeriodInputForChange(value2, minDate, maxDate);
186
+ if (parseResult.type === "invalid-start") {
187
+ setTextValue("");
188
+ handleSelect([null, null]);
189
+ return;
109
190
  }
110
- if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) {
111
- handleSelect([startDateValid, endDateValid]);
191
+ if (parseResult.type === "invalid-end") {
192
+ setTextValue("");
193
+ return;
112
194
  }
195
+ if (parseResult.type === "complete") handleSelect(parseResult.value);
113
196
  };
114
197
  const handleBlur = (e) => {
115
198
  setActive(false);
116
- const value2 = e.target.value;
117
- const valueParts = value2.split(DELIMITER);
118
- const startDateValid = valueParts[0] ? validateDate(valueParts[0], minDate, maxDate) : null;
119
- const endDateValid = valueParts[1] ? validateDate(valueParts[1], minDate, maxDate) : null;
120
- if (!startDateValid || !endDateValid || startDateValid && endDateValid && endDateValid.getTime() < startDateValid.getTime()) {
199
+ const parsedPeriod = parseStrictPeriodValue(e.target.value, minDate, maxDate);
200
+ if (!parsedPeriod) {
121
201
  setTextValue("");
122
202
  handleSelect([null, null]);
123
203
  }
@@ -131,7 +211,7 @@ const Period = React.forwardRef((props, ref) => {
131
211
  /* @__PURE__ */ jsxRuntime.jsx(ui_ButtonIcon_ButtonIcon.default, { "data-calendar-btn": true, size: "medium", "data-testid": "period-btn", variant: "transparent", color: "neutral", disabled, onClick: handleClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "material_icon", ref: btnRef, children: "calendar_today" }) })
132
212
  ] });
133
213
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "arm-period", "data-testid": "period", children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(ui_Form_TextField_TextField.TextField, { ref, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
214
+ /* @__PURE__ */ jsxRuntime.jsx(ui_Form_TextField_TextField.TextField, { ref: setInputRef, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
135
215
  active && /* @__PURE__ */ jsxRuntime.jsx(ui_Popper_Popper.Popper, { anchorEl: btnRef.current, open: active, placement: "bottom-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui_Card_Card.default, { className: "arm-period__picker", "data-testid": "period_card", children: /* @__PURE__ */ jsxRuntime.jsx(ui_Form_DatePicker_DatePicker.default, { onClickOutside: handleClickOutside, onChange: handleSelect, selectsRange: true, selected: realValue[0], startDate: realValue[0], endDate: realValue[1], minDate: minDate || void 0, maxDate: maxDate || void 0, showMonthDropdown, showYearDropdown, ...datePickerProps }) }) })
136
216
  ] }) });
137
217
  });
@@ -39,6 +39,8 @@ function Select(props, ref) {
39
39
  beforeList,
40
40
  query,
41
41
  optionClassName,
42
+ onBlur: inputOnBlur,
43
+ onFocus: inputOnFocus,
42
44
  ...inputProps
43
45
  } = props;
44
46
  let [active, setActive] = React.useState(!!defaultOpen);
@@ -51,6 +53,40 @@ function Select(props, ref) {
51
53
  React.useImperativeHandle(ref, () => {
52
54
  return inputRef.current;
53
55
  }, []);
56
+ React.useLayoutEffect(() => {
57
+ var _a2;
58
+ if (value !== void 0 || multiple || search) return;
59
+ if (!inputRef.current) return;
60
+ const inputElement = inputRef.current;
61
+ const syncSelectedValue = (nextInputValue) => {
62
+ setSelected((prevSelected) => {
63
+ if (Array.isArray(prevSelected)) return prevSelected;
64
+ if (!nextInputValue) return prevSelected === "" ? prevSelected : "";
65
+ const optionExists = options.some((item) => getOptionValue(item) === nextInputValue);
66
+ if (!optionExists || prevSelected === nextInputValue) return prevSelected;
67
+ return nextInputValue;
68
+ });
69
+ };
70
+ syncSelectedValue(inputElement.value);
71
+ const inputValueDescriptor = Object.getOwnPropertyDescriptor(inputElement, "value");
72
+ if (!(inputValueDescriptor == null ? void 0 : inputValueDescriptor.get) || !(inputValueDescriptor == null ? void 0 : inputValueDescriptor.set)) return;
73
+ Object.defineProperty(inputElement, "value", {
74
+ configurable: true,
75
+ enumerable: (_a2 = inputValueDescriptor.enumerable) != null ? _a2 : true,
76
+ get() {
77
+ var _a3;
78
+ return (_a3 = inputValueDescriptor.get) == null ? void 0 : _a3.call(this);
79
+ },
80
+ set(nextValue) {
81
+ var _a3;
82
+ (_a3 = inputValueDescriptor.set) == null ? void 0 : _a3.call(this, nextValue);
83
+ syncSelectedValue(nextValue);
84
+ }
85
+ });
86
+ return () => {
87
+ Object.defineProperty(inputElement, "value", inputValueDescriptor);
88
+ };
89
+ }, [multiple, options, search, value]);
54
90
  const handleOpen = () => {
55
91
  if (open !== void 0) return;
56
92
  if (!inputProps.disabled) {
@@ -94,12 +130,14 @@ function Select(props, ref) {
94
130
  const handleSearch = (e) => {
95
131
  setQ(e.target.value);
96
132
  };
97
- const handleFocus = search ? () => {
133
+ const handleFocus = search ? (e) => {
98
134
  setFocused(true);
99
- } : void 0;
100
- const handleBlur = !!search ? () => {
135
+ if (inputOnFocus) inputOnFocus(e);
136
+ } : inputOnFocus;
137
+ const handleBlur = !!search ? (e) => {
101
138
  setFocused(false);
102
- } : void 0;
139
+ if (inputOnBlur) inputOnBlur(e);
140
+ } : inputOnBlur;
103
141
  const handleSelectAll = (e) => {
104
142
  if (props.disabled) return;
105
143
  let value2 = options.filter((item) => !item.disabled).map((item) => getOptionValue(item));
@@ -4,6 +4,6 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const ui_Form_DateField_DateField = require("../DateField/DateField.cjs");
5
5
  const React = require("react");
6
6
  const TimeField = React.forwardRef((props, ref) => {
7
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(ui_Form_DateField_DateField.DateField, { ...props, showTime: true, ref, showTimeOnly: true }) });
7
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(ui_Form_DateField_DateField.DateField, { ...props, showTime: true, ref, showTimeOnly: true, preserveIncompleteTimeInput: true }) });
8
8
  });
9
9
  exports.default = TimeField;
@@ -5,6 +5,7 @@ declare const TimeField: import("react").ForwardRefExoticComponent<{
5
5
  value?: Date | string | null;
6
6
  showTime?: boolean;
7
7
  showTimeOnly?: boolean;
8
+ preserveIncompleteTimeInput?: boolean;
8
9
  inputProps?: Omit<import("../TextField").TextFieldProps, "onChange" | "value"> & import("../../../types/theme").DataAttributes;
9
10
  showMonthYearPicker?: boolean;
10
11
  showYearPicker?: boolean;
@@ -21,6 +22,7 @@ declare const TimeField: import("react").ForwardRefExoticComponent<{
21
22
  value?: Date | string | null;
22
23
  showTime?: boolean;
23
24
  showTimeOnly?: boolean;
25
+ preserveIncompleteTimeInput?: boolean;
24
26
  inputProps?: Omit<import("../TextField").TextFieldProps, "onChange" | "value"> & import("../../../types/theme").DataAttributes;
25
27
  showMonthYearPicker?: boolean;
26
28
  showYearPicker?: boolean;
@@ -15,6 +15,7 @@ type OwnProps = {
15
15
  value?: Date | string | null;
16
16
  showTime?: boolean;
17
17
  showTimeOnly?: boolean;
18
+ preserveIncompleteTimeInput?: boolean;
18
19
  inputProps?: Omit<TextFieldProps, 'onChange' | 'value'> & DataAttributes;
19
20
  showMonthYearPicker?: boolean;
20
21
  showYearPicker?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import { TextField } from "../TextField/TextField.js";
3
- import { forwardRef, useState, useEffect, useRef } from "react";
3
+ import { forwardRef, useState, useRef, useEffect, useLayoutEffect } from "react";
4
4
  import Card from "../../Card/Card.js";
5
5
  import DatePicker from "../DatePicker/DatePicker.js";
6
6
  import ButtonIcon from "../../ButtonIcon/ButtonIcon.js";
@@ -36,6 +36,7 @@ const DateField = forwardRef((props, ref) => {
36
36
  showTimeOnly,
37
37
  disabled,
38
38
  value,
39
+ preserveIncompleteTimeInput,
39
40
  ...datepickerProps
40
41
  } = props;
41
42
  let format = "dd.MM.yyyy";
@@ -44,9 +45,70 @@ const DateField = forwardRef((props, ref) => {
44
45
  const [date, setDate] = useState(null);
45
46
  const [open, setOpen] = useState(false);
46
47
  const [text, setText] = useState(dateToText(value, format));
48
+ const inputRef = useRef(null);
49
+ const setInputRef = (node) => {
50
+ inputRef.current = node;
51
+ if (typeof ref === "function") ref(node);
52
+ else if (ref) ref.current = node;
53
+ };
47
54
  useEffect(() => {
48
55
  if (value) setText(dateToText(value, format));
49
56
  }, [value]);
57
+ const syncFromInputValue = (nextInputValue) => {
58
+ if (value !== void 0) return;
59
+ if (!nextInputValue) {
60
+ setText("");
61
+ setDate(null);
62
+ return;
63
+ }
64
+ let parsedDate = null;
65
+ const parsedTimestamp = Date.parse(nextInputValue);
66
+ if (!Number.isNaN(parsedTimestamp)) parsedDate = new Date(parsedTimestamp);
67
+ if (showTimeOnly) {
68
+ if (preserveIncompleteTimeInput && /^\d{1,2}:?$/.test(nextInputValue)) {
69
+ setText(nextInputValue.endsWith(":") ? nextInputValue : `${nextInputValue}:`);
70
+ setDate(null);
71
+ return;
72
+ }
73
+ const timeParts = nextInputValue.split(":").map(Number);
74
+ if (timeParts.length >= 2 && Number.isFinite(timeParts[0]) && Number.isFinite(timeParts[1])) {
75
+ const today = /* @__PURE__ */ new Date();
76
+ today.setHours(timeParts[0], timeParts[1], timeParts[2] || 0, 0);
77
+ parsedDate = new Date(today.getTime());
78
+ }
79
+ }
80
+ if (!parsedDate) {
81
+ setText(nextInputValue);
82
+ setDate(null);
83
+ return;
84
+ }
85
+ setDate(parsedDate);
86
+ setText(dateToText(parsedDate, format));
87
+ };
88
+ useLayoutEffect(() => {
89
+ var _a;
90
+ if (value !== void 0 || !inputRef.current) return;
91
+ const inputElement = inputRef.current;
92
+ syncFromInputValue(inputElement.value);
93
+ const inputValueDescriptor = Object.getOwnPropertyDescriptor(inputElement, "value");
94
+ if (!(inputValueDescriptor == null ? void 0 : inputValueDescriptor.get) || !(inputValueDescriptor == null ? void 0 : inputValueDescriptor.set)) return;
95
+ Object.defineProperty(inputElement, "value", {
96
+ configurable: true,
97
+ enumerable: (_a = inputValueDescriptor.enumerable) != null ? _a : true,
98
+ get() {
99
+ var _a2;
100
+ return (_a2 = inputValueDescriptor.get) == null ? void 0 : _a2.call(this);
101
+ },
102
+ set(nextValue) {
103
+ var _a2;
104
+ (_a2 = inputValueDescriptor.set) == null ? void 0 : _a2.call(this, nextValue);
105
+ syncFromInputValue(nextValue);
106
+ }
107
+ });
108
+ return () => {
109
+ Object.defineProperty(inputElement, "value", inputValueDescriptor);
110
+ };
111
+ }, [format, preserveIncompleteTimeInput, showTimeOnly, value]);
50
112
  const handleChange = (d) => {
51
113
  setDate(d);
52
114
  if (!showTime || showTime && d && d.getHours() > 0) setOpen(false);
@@ -139,7 +201,7 @@ const DateField = forwardRef((props, ref) => {
139
201
  if (!(dateValue instanceof Date && !isNaN(dateValue.getDate()))) dateValue = null;
140
202
  const calendarBtn = /* @__PURE__ */ jsx(ButtonIcon, { disabled, size: "medium", variant: "transparent", onClick: handleClick, color: "neutral", "data-testid": "datefield-btn", children: /* @__PURE__ */ jsx("span", { ref: btnRef, className: "material_icon", children: inputIcon }) });
141
203
  return /* @__PURE__ */ jsxs(Fragment, { children: [
142
- /* @__PURE__ */ jsx(TextField, { ref, label, placeholder, className, error, helperText, ...inputProps, value: text || "", disabled, onChange: handleChangeText, onPaste: handlePaste, endAdornment: calendarBtn }),
204
+ /* @__PURE__ */ jsx(TextField, { ref: setInputRef, label, placeholder, className, error, helperText, ...inputProps, value: text || "", disabled, onChange: handleChangeText, onPaste: handlePaste, endAdornment: calendarBtn }),
143
205
  open && /* @__PURE__ */ jsx(BackDrop, { onClick: () => setOpen(false), children: /* @__PURE__ */ jsx(PopperBase, { anchorEl: btnRef.current, className: "arm-datefield-popper", open, placement: "bottom-end", children: /* @__PURE__ */ jsx("div", { style: {
144
206
  backgroundColor: "#fff",
145
207
  width: "100%"
@@ -9,12 +9,15 @@ import { AdornmentContainer } from "../../Adornment/Adornment.js";
9
9
  import { Popper } from "../../Popper/Popper.js";
10
10
  import "../../Popper/PopperBase.js";
11
11
  const DELIMITER = " - ";
12
+ const COMPLETE_DATE_REGEX = /^\d{2}\.\d{2}\.\d{4}$/;
13
+ const COMPLETE_PERIOD_REGEX = new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`);
12
14
  const format = "dd.MM.yyyy";
13
15
  function getPeriodDisplay(date1, date2) {
14
16
  if (date1 && date2) return DateService(date1).format(format) + DELIMITER + DateService(date2).format(format);
15
17
  return "";
16
18
  }
17
19
  function validateDate(date, minDate, maxDate) {
20
+ if (!COMPLETE_DATE_REGEX.test(date)) return null;
18
21
  const startParts = date.split(".");
19
22
  const startTimeStamp = Date.parse(`${startParts[2]}-${startParts[1]}-${startParts[0]}`);
20
23
  if (isNaN(startTimeStamp) === false) {
@@ -25,6 +28,55 @@ function validateDate(date, minDate, maxDate) {
25
28
  }
26
29
  return null;
27
30
  }
31
+ function parseSerializedDateTuple(value) {
32
+ const boundaryIndex = value.indexOf("),");
33
+ if (boundaryIndex < 0) return null;
34
+ const leftPart = value.slice(0, boundaryIndex + 1).trim();
35
+ const rightPart = value.slice(boundaryIndex + 2).trim();
36
+ const leftDate = new Date(leftPart);
37
+ const rightDate = new Date(rightPart);
38
+ if (Number.isNaN(leftDate.getTime()) || Number.isNaN(rightDate.getTime()) || rightDate.getTime() < leftDate.getTime()) return null;
39
+ return [leftDate, rightDate];
40
+ }
41
+ function isBackspaceAtPeriodBoundary(prevValue, nextValue) {
42
+ return (prevValue.endsWith(".") || prevValue.endsWith(" ") || prevValue.endsWith("-")) && prevValue.length > nextValue.length;
43
+ }
44
+ function normalizePeriodInput(rawValue) {
45
+ return rawValue.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})(\\d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})(\\d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
46
+ }
47
+ function parsePeriodInputForChange(value, minDate, maxDate) {
48
+ const valueParts = value.split(DELIMITER);
49
+ let startDateValid = null;
50
+ let endDateValid = null;
51
+ if (COMPLETE_DATE_REGEX.test(valueParts[0])) {
52
+ startDateValid = validateDate(valueParts[0], minDate, maxDate);
53
+ if (!startDateValid) return {
54
+ type: "invalid-start"
55
+ };
56
+ }
57
+ if (COMPLETE_PERIOD_REGEX.test(value) && valueParts.length === 2) {
58
+ endDateValid = validateDate(valueParts[1], minDate, maxDate);
59
+ if (!endDateValid) return {
60
+ type: "invalid-end"
61
+ };
62
+ }
63
+ if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) return {
64
+ type: "complete",
65
+ value: [startDateValid, endDateValid]
66
+ };
67
+ return {
68
+ type: "partial"
69
+ };
70
+ }
71
+ function parseStrictPeriodValue(value, minDate, maxDate) {
72
+ const valueParts = value.split(DELIMITER);
73
+ if (valueParts.length !== 2) return null;
74
+ const startDateValid = validateDate(valueParts[0], minDate, maxDate);
75
+ const endDateValid = validateDate(valueParts[1], minDate, maxDate);
76
+ if (!startDateValid || !endDateValid) return null;
77
+ if (endDateValid.getTime() < startDateValid.getTime()) return null;
78
+ return [startDateValid, endDateValid];
79
+ }
28
80
  const Period = forwardRef((props, ref) => {
29
81
  const {
30
82
  onChange,
@@ -45,11 +97,56 @@ const Period = forwardRef((props, ref) => {
45
97
  const displayText = defaultValue ? getPeriodDisplay(defaultValue[0], defaultValue[1]) : "";
46
98
  const [textValue, setTextValue] = useState(displayText);
47
99
  const btnRef = useRef();
100
+ const inputRef = useRef(null);
101
+ const didSyncFromRefValueRef = useRef(false);
102
+ const datesEqual = (left, right) => {
103
+ var _a, _b, _c, _d;
104
+ return (((_a = left[0]) == null ? void 0 : _a.getTime()) || 0) === (((_b = right[0]) == null ? void 0 : _b.getTime()) || 0) && (((_c = left[1]) == null ? void 0 : _c.getTime()) || 0) === (((_d = right[1]) == null ? void 0 : _d.getTime()) || 0);
105
+ };
106
+ const setInputRef = (node) => {
107
+ inputRef.current = node;
108
+ if (typeof ref === "function") ref(node);
109
+ else if (ref) ref.current = node;
110
+ if (node && !Array.isArray(props.value) && !didSyncFromRefValueRef.current) {
111
+ syncFromInputValue(node.value);
112
+ didSyncFromRefValueRef.current = true;
113
+ }
114
+ };
48
115
  const [startValue, endValue] = value || [];
49
116
  useEffect(() => {
50
117
  if (startValue && endValue) setTextValue(getPeriodDisplay(startValue, endValue));
51
118
  else if (startValue === null && endValue === null) setTextValue("");
52
119
  }, [startValue, endValue]);
120
+ const syncFromInputValue = (nextInputValue) => {
121
+ if (Array.isArray(props.value)) return;
122
+ if (!nextInputValue) {
123
+ setTextValue((prev) => prev === "" ? prev : "");
124
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
125
+ return;
126
+ }
127
+ const valueParts = nextInputValue.split(DELIMITER);
128
+ if (valueParts.length !== 2) {
129
+ const serializedDates = parseSerializedDateTuple(nextInputValue);
130
+ if (!serializedDates) return;
131
+ const normalizedText2 = getPeriodDisplay(serializedDates[0], serializedDates[1]);
132
+ setCurrentValue((prev) => datesEqual(prev, serializedDates) ? prev : serializedDates);
133
+ setTextValue((prev) => prev === normalizedText2 ? prev : normalizedText2);
134
+ return;
135
+ }
136
+ const startDate = validateDate(valueParts[0], minDate, maxDate);
137
+ const endDate = validateDate(valueParts[1], minDate, maxDate);
138
+ if (!startDate || !endDate || endDate.getTime() < startDate.getTime()) {
139
+ setTextValue((prev) => prev === nextInputValue ? prev : nextInputValue);
140
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
141
+ return;
142
+ }
143
+ const normalizedText = getPeriodDisplay(startDate, endDate);
144
+ setCurrentValue((prev) => {
145
+ const nextPeriod = [startDate, endDate];
146
+ return datesEqual(prev, nextPeriod) ? prev : nextPeriod;
147
+ });
148
+ setTextValue((prev) => prev === normalizedText ? prev : normalizedText);
149
+ };
53
150
  const handleClick = (e) => {
54
151
  e.stopPropagation();
55
152
  setActive((prev) => !prev);
@@ -79,43 +176,26 @@ const Period = forwardRef((props, ref) => {
79
176
  };
80
177
  const handleChange = (e) => {
81
178
  if (disabled) return;
82
- let value2 = e.target.value;
83
- const bs = (textValue.endsWith(".") || textValue.endsWith(" ") || textValue.endsWith("-")) && textValue.length > value2.length;
84
- if (!bs) {
85
- value2 = value2.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})(d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})(d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
86
- }
87
- let startDateValid = null;
88
- let endDateValid = null;
89
- const valueParts = value2.split(DELIMITER);
90
- if (valueParts[0].match(/^\d{2}\.\d{2}\.\d{4}$/)) {
91
- startDateValid = validateDate(valueParts[0], minDate, maxDate);
92
- if (!startDateValid) {
93
- setTextValue("");
94
- handleSelect([null, null]);
95
- return;
96
- }
97
- }
179
+ const rawValue = e.target.value;
180
+ const value2 = isBackspaceAtPeriodBoundary(textValue, rawValue) ? rawValue : normalizePeriodInput(rawValue);
98
181
  setTextValue(value2);
99
182
  setCurrentValue([null, null]);
100
- if (value2.match(new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`))) {
101
- if (valueParts.length === 2) {
102
- endDateValid = validateDate(valueParts[1], minDate, maxDate);
103
- if (!endDateValid) {
104
- setTextValue("");
105
- }
106
- }
183
+ const parseResult = parsePeriodInputForChange(value2, minDate, maxDate);
184
+ if (parseResult.type === "invalid-start") {
185
+ setTextValue("");
186
+ handleSelect([null, null]);
187
+ return;
107
188
  }
108
- if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) {
109
- handleSelect([startDateValid, endDateValid]);
189
+ if (parseResult.type === "invalid-end") {
190
+ setTextValue("");
191
+ return;
110
192
  }
193
+ if (parseResult.type === "complete") handleSelect(parseResult.value);
111
194
  };
112
195
  const handleBlur = (e) => {
113
196
  setActive(false);
114
- const value2 = e.target.value;
115
- const valueParts = value2.split(DELIMITER);
116
- const startDateValid = valueParts[0] ? validateDate(valueParts[0], minDate, maxDate) : null;
117
- const endDateValid = valueParts[1] ? validateDate(valueParts[1], minDate, maxDate) : null;
118
- if (!startDateValid || !endDateValid || startDateValid && endDateValid && endDateValid.getTime() < startDateValid.getTime()) {
197
+ const parsedPeriod = parseStrictPeriodValue(e.target.value, minDate, maxDate);
198
+ if (!parsedPeriod) {
119
199
  setTextValue("");
120
200
  handleSelect([null, null]);
121
201
  }
@@ -129,7 +209,7 @@ const Period = forwardRef((props, ref) => {
129
209
  /* @__PURE__ */ jsx(ButtonIcon, { "data-calendar-btn": true, size: "medium", "data-testid": "period-btn", variant: "transparent", color: "neutral", disabled, onClick: handleClick, children: /* @__PURE__ */ jsx("span", { className: "material_icon", ref: btnRef, children: "calendar_today" }) })
130
210
  ] });
131
211
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "arm-period", "data-testid": "period", children: [
132
- /* @__PURE__ */ jsx(TextField, { ref, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
212
+ /* @__PURE__ */ jsx(TextField, { ref: setInputRef, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
133
213
  active && /* @__PURE__ */ jsx(Popper, { anchorEl: btnRef.current, open: active, placement: "bottom-end", children: /* @__PURE__ */ jsx(Card, { className: "arm-period__picker", "data-testid": "period_card", children: /* @__PURE__ */ jsx(DatePicker, { onClickOutside: handleClickOutside, onChange: handleSelect, selectsRange: true, selected: realValue[0], startDate: realValue[0], endDate: realValue[1], minDate: minDate || void 0, maxDate: maxDate || void 0, showMonthDropdown, showYearDropdown, ...datePickerProps }) }) })
134
214
  ] }) });
135
215
  });