@cloud-ru/ds-fields 2.2.3-preview-8025692f.0 → 2.2.3-preview-a0540d7f.0
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.
- package/README.md +4 -2
- package/dist/cjs/components/FieldDate/FieldDate.js +3 -3
- package/dist/cjs/components/FieldDate/mask.d.ts +2 -3
- package/dist/cjs/components/FieldDate/mask.js +3 -4
- package/dist/cjs/components/FieldSelect/FieldSelect.js +98 -43
- package/dist/cjs/components/FieldSelect/constants.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/constants.js +20 -1
- package/dist/cjs/components/FieldSelect/hooks.d.ts +33 -0
- package/dist/cjs/components/FieldSelect/hooks.js +95 -0
- package/dist/cjs/components/FieldSelect/types.d.ts +26 -3
- package/dist/cjs/components/FieldSelect/utils/customOption.d.ts +21 -0
- package/dist/cjs/components/FieldSelect/utils/customOption.js +55 -0
- package/dist/cjs/components/FieldSelect/utils/extract.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/utils/extract.js +39 -0
- package/dist/cjs/components/FieldSelect/utils/filter.d.ts +3 -0
- package/dist/cjs/components/FieldSelect/utils/filter.js +40 -0
- package/dist/cjs/components/FieldSelect/utils/index.d.ts +6 -0
- package/dist/cjs/components/FieldSelect/utils/index.js +22 -0
- package/dist/cjs/components/FieldSelect/utils/items.d.ts +9 -0
- package/dist/cjs/components/FieldSelect/utils/items.js +47 -0
- package/dist/cjs/components/FieldSelect/utils/selection.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/utils/selection.js +18 -0
- package/dist/cjs/components/FieldSelect/utils/tagSize.d.ts +4 -0
- package/dist/cjs/components/FieldSelect/utils/tagSize.js +5 -0
- package/dist/cjs/components/FieldTime/FieldTime.js +2 -2
- package/dist/cjs/segments/useSegmentedMask.d.ts +0 -2
- package/dist/cjs/segments/useSegmentedMask.js +56 -146
- package/dist/esm/components/FieldDate/FieldDate.js +3 -3
- package/dist/esm/components/FieldDate/mask.d.ts +2 -3
- package/dist/esm/components/FieldDate/mask.js +3 -4
- package/dist/esm/components/FieldSelect/FieldSelect.js +100 -45
- package/dist/esm/components/FieldSelect/constants.d.ts +11 -0
- package/dist/esm/components/FieldSelect/constants.js +19 -0
- package/dist/esm/components/FieldSelect/hooks.d.ts +33 -0
- package/dist/esm/components/FieldSelect/hooks.js +92 -0
- package/dist/esm/components/FieldSelect/types.d.ts +26 -3
- package/dist/esm/components/FieldSelect/utils/customOption.d.ts +21 -0
- package/dist/esm/components/FieldSelect/utils/customOption.js +48 -0
- package/dist/esm/components/FieldSelect/utils/extract.d.ts +11 -0
- package/dist/esm/components/FieldSelect/utils/extract.js +34 -0
- package/dist/esm/components/FieldSelect/utils/filter.d.ts +3 -0
- package/dist/esm/components/FieldSelect/utils/filter.js +36 -0
- package/dist/esm/components/FieldSelect/utils/index.d.ts +6 -0
- package/dist/esm/components/FieldSelect/utils/index.js +6 -0
- package/dist/esm/components/FieldSelect/utils/items.d.ts +9 -0
- package/dist/esm/components/FieldSelect/utils/items.js +41 -0
- package/dist/esm/components/FieldSelect/utils/selection.d.ts +11 -0
- package/dist/esm/components/FieldSelect/utils/selection.js +14 -0
- package/dist/esm/components/FieldSelect/utils/tagSize.d.ts +4 -0
- package/dist/esm/components/FieldSelect/utils/tagSize.js +2 -0
- package/dist/esm/components/FieldTime/FieldTime.js +2 -2
- package/dist/esm/segments/useSegmentedMask.d.ts +0 -2
- package/dist/esm/segments/useSegmentedMask.js +56 -146
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/components/FieldDate/FieldDate.tsx +3 -3
- package/src/components/FieldDate/mask.ts +3 -4
- package/src/components/FieldSelect/FieldSelect.tsx +151 -63
- package/src/components/FieldSelect/constants.ts +22 -0
- package/src/components/FieldSelect/hooks.ts +177 -0
- package/src/components/FieldSelect/types.ts +82 -48
- package/src/components/FieldSelect/utils/customOption.ts +77 -0
- package/src/components/FieldSelect/utils/extract.ts +49 -0
- package/src/components/FieldSelect/utils/filter.ts +51 -0
- package/src/components/FieldSelect/utils/index.ts +6 -0
- package/src/components/FieldSelect/utils/items.ts +58 -0
- package/src/components/FieldSelect/utils/selection.ts +25 -0
- package/src/components/FieldSelect/utils/tagSize.ts +5 -0
- package/src/components/FieldTime/FieldTime.tsx +2 -2
- package/src/segments/useSegmentedMask.ts +52 -172
- package/dist/cjs/components/FieldSelect/utils.d.ts +0 -19
- package/dist/cjs/components/FieldSelect/utils.js +0 -103
- package/dist/esm/components/FieldSelect/utils.d.ts +0 -19
- package/dist/esm/components/FieldSelect/utils.js +0 -92
- package/src/components/FieldSelect/utils.ts +0 -132
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { extractSearchText } from './extract.js';
|
|
2
|
+
// Subsequence-fuzzy: символы запроса должны встречаться в строке в исходном порядке.
|
|
3
|
+
// Пример: query=`lge` matches `Large` (L → r → arg → e).
|
|
4
|
+
export function isFuzzyMatch(haystack, needle) {
|
|
5
|
+
if (!needle) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
let i = 0;
|
|
9
|
+
for (const ch of haystack) {
|
|
10
|
+
if (ch === needle[i]) {
|
|
11
|
+
i += 1;
|
|
12
|
+
if (i === needle.length) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function filterItems(items, query, fuzzy) {
|
|
20
|
+
if (!query) {
|
|
21
|
+
return items;
|
|
22
|
+
}
|
|
23
|
+
const q = query.toLowerCase();
|
|
24
|
+
const matches = (item) => {
|
|
25
|
+
const text = extractSearchText(item).toLowerCase();
|
|
26
|
+
return fuzzy ? isFuzzyMatch(text, q) : text.includes(q);
|
|
27
|
+
};
|
|
28
|
+
const walk = (list) => list.flatMap(item => {
|
|
29
|
+
if ('items' in item && Array.isArray(item.items)) {
|
|
30
|
+
const filteredChildren = walk(item.items);
|
|
31
|
+
return filteredChildren.length > 0 ? [{ ...item, items: filteredChildren }] : [];
|
|
32
|
+
}
|
|
33
|
+
return matches(item) ? [item] : [];
|
|
34
|
+
});
|
|
35
|
+
return walk(items);
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ItemId } from '@cloud-ru/ds-list';
|
|
2
|
+
import { FieldSelectItem } from '../types.js';
|
|
3
|
+
import { WithIdContent } from './extract.js';
|
|
4
|
+
export declare function flatten(items: FieldSelectItem[]): WithIdContent[];
|
|
5
|
+
export declare function findItem(items: FieldSelectItem[], id: ItemId): WithIdContent | undefined;
|
|
6
|
+
/** Айтем для выбранного значения, которого нет в `items` (кастомная опция, ленивая подгрузка). */
|
|
7
|
+
export declare function createPlaceholderItem(id: ItemId): FieldSelectItem;
|
|
8
|
+
/** Выбранные id, которых нет в дереве айтемов — в дроплист их нужно добавить отдельно. */
|
|
9
|
+
export declare function getMissingSelectedItems(selectedIds: ItemId[], allItems: FieldSelectItem[]): FieldSelectItem[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function flatten(items) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (const item of items) {
|
|
4
|
+
out.push(item);
|
|
5
|
+
if ('items' in item && Array.isArray(item.items)) {
|
|
6
|
+
out.push(...flatten(item.items));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
export function findItem(items, id) {
|
|
12
|
+
for (const item of flatten(items)) {
|
|
13
|
+
if (item.id === id) {
|
|
14
|
+
return item;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Айтем для выбранного значения, которого нет в `items` (кастомная опция, ленивая подгрузка). */
|
|
20
|
+
export function createPlaceholderItem(id) {
|
|
21
|
+
return { id, content: { label: String(id) } };
|
|
22
|
+
}
|
|
23
|
+
/** Выбранные id, которых нет в дереве айтемов — в дроплист их нужно добавить отдельно. */
|
|
24
|
+
export function getMissingSelectedItems(selectedIds, allItems) {
|
|
25
|
+
if (selectedIds.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const existingIds = new Set();
|
|
29
|
+
for (const item of flatten(allItems)) {
|
|
30
|
+
if (item.id !== undefined) {
|
|
31
|
+
existingIds.add(item.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const missing = [];
|
|
35
|
+
for (const id of selectedIds) {
|
|
36
|
+
if (id !== '' && !existingIds.has(id)) {
|
|
37
|
+
missing.push(createPlaceholderItem(id));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return missing;
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ItemId } from '@cloud-ru/ds-list';
|
|
2
|
+
import { FieldSelectMultipleProps, Selection } from '../types.js';
|
|
3
|
+
export declare function isMultiple(props: {
|
|
4
|
+
selection?: Selection;
|
|
5
|
+
}): props is FieldSelectMultipleProps;
|
|
6
|
+
/** Выбранные id в нормализованном виде: массив и для `single`, и для `multiple`. */
|
|
7
|
+
export declare function getSelectedIds(params: {
|
|
8
|
+
multiple: boolean;
|
|
9
|
+
multipleValue: ItemId[];
|
|
10
|
+
singleValue: ItemId | undefined;
|
|
11
|
+
}): ItemId[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SELECTION_MODE } from '../constants.js';
|
|
2
|
+
export function isMultiple(props) {
|
|
3
|
+
return props.selection === SELECTION_MODE.Multiple;
|
|
4
|
+
}
|
|
5
|
+
/** Выбранные id в нормализованном виде: массив и для `single`, и для `multiple`. */
|
|
6
|
+
export function getSelectedIds(params) {
|
|
7
|
+
if (params.multiple) {
|
|
8
|
+
return params.multipleValue;
|
|
9
|
+
}
|
|
10
|
+
if (params.singleValue === undefined) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return [params.singleValue];
|
|
14
|
+
}
|
|
@@ -131,7 +131,7 @@ export const FieldTime = forwardRef(function FieldTime({ label = '', labelToolti
|
|
|
131
131
|
// clear/copy недостижимыми со стрелок — движок всегда делает preventDefault на ArrowRight.
|
|
132
132
|
onInputKeyDown(event);
|
|
133
133
|
}, [onInputKeyDown, timeSeg]);
|
|
134
|
-
// value ведёт сегментный движок (
|
|
134
|
+
// value ведёт сегментный движок (keydown); нативный onChange input'а игнорируем.
|
|
135
135
|
const handleInputChange = useCallback(() => undefined, []);
|
|
136
136
|
const handleInputFocus = useCallback((event) => {
|
|
137
137
|
setFocusVisible(event.target.matches(':focus-visible'));
|
|
@@ -149,7 +149,7 @@ export const FieldTime = forwardRef(function FieldTime({ label = '', labelToolti
|
|
|
149
149
|
readonly: readOnly,
|
|
150
150
|
hover,
|
|
151
151
|
focusVisible: focusVisible || open,
|
|
152
|
-
}), children: _jsx("div", { className: fieldStyles.acrylicBg, "aria-hidden": true }) }), _jsx("div", { className: fieldStyles.borderStateLayer, "data-state": 'borderOnBackground' }), _jsx("div", { className: fieldStyles.focusLayer })] }), _jsx("div", { className: fieldStyles.fieldContainer, children: _jsxs("div", { className: fieldStyles.contentWrapper, children: [_jsx("div", { className: fieldStyles.inputLine, children: _jsxs("div", { className: fieldStyles.inputArea, children: [_jsx(InputPrivate, { ref: mergeRefs(ref, localRef
|
|
152
|
+
}), children: _jsx("div", { className: fieldStyles.acrylicBg, "aria-hidden": true }) }), _jsx("div", { className: fieldStyles.borderStateLayer, "data-state": 'borderOnBackground' }), _jsx("div", { className: fieldStyles.focusLayer })] }), _jsx("div", { className: fieldStyles.fieldContainer, children: _jsxs("div", { className: fieldStyles.contentWrapper, children: [_jsx("div", { className: fieldStyles.inputLine, children: _jsxs("div", { className: fieldStyles.inputArea, children: [_jsx(InputPrivate, { ref: mergeRefs(ref, localRef), className: fieldStyles.fieldInput, value: inputValue, onChange: handleInputChange, disabled: disabled, readonly: readOnly, placeholder: placeholderMask, inputMode: INPUT_MODE.Numeric, tabIndex: inputTabIndex, onClick: timeSeg.handleClick, onPaste: timeSeg.handlePaste, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleInputKeyDown, id: id, name: name, autoFocus: resolvedAutoFocus, "aria-haspopup": 'dialog', "aria-expanded": open, ...extractSupportProps(rest), "data-test-id": TEST_IDS.fieldTimeInput }), postfixButtons && _jsx("span", { className: fieldStyles.postfixButtonsSlot, children: postfixButtons })] }) }), _jsx("div", { className: fieldStyles.iconSlot, "data-test-id": TEST_IDS.fieldTimeIcon, "aria-hidden": true, children: _jsx(WatchSVG, {}) })] }) })] }));
|
|
153
153
|
return (_jsx(FieldDecorator, { className: className, label: label, labelTooltip: labelTooltip, caption: caption, hint: hint, error: error, validationState: validationState, showHintIcon: showHintIcon, length: length, required: required, labelFor: id, disabled: disabled, readonly: readOnly, size: size, children: _jsx(TimePickerDropdown
|
|
154
154
|
// Явный data-test-id на дропдауне → детерминированный id портального контента:
|
|
155
155
|
// `getTestIdBuilder` строит `content-${id}`, так контент адресуем для open-picker-снимка
|
|
@@ -29,7 +29,6 @@ type UseSegmentedMaskParams = {
|
|
|
29
29
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
30
30
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
31
31
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
32
|
-
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
33
32
|
*/
|
|
34
33
|
export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }: UseSegmentedMaskParams): {
|
|
35
34
|
handleKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -37,7 +36,6 @@ export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disa
|
|
|
37
36
|
handleFocus: () => void;
|
|
38
37
|
handleBlur: () => void;
|
|
39
38
|
handlePaste: (event: ClipboardEvent<HTMLInputElement>) => void;
|
|
40
|
-
nativeInputRef: (node: HTMLInputElement | null) => void;
|
|
41
39
|
selectSlot: (slot: SlotMeta) => void;
|
|
42
40
|
firstSlot: SlotMeta;
|
|
43
41
|
lastSlot: SlotMeta;
|
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import { useEventHandler } from '@cloud-ru/ds-utils';
|
|
2
1
|
import { useCallback, useRef } from 'react';
|
|
3
|
-
const RESET_TYPED_KEYS = new Set([
|
|
4
|
-
'ArrowLeft',
|
|
5
|
-
'ArrowRight',
|
|
6
|
-
'ArrowUp',
|
|
7
|
-
'ArrowDown',
|
|
8
|
-
'Backspace',
|
|
9
|
-
'Delete',
|
|
10
|
-
'Enter',
|
|
11
|
-
'Tab',
|
|
12
|
-
'Escape',
|
|
13
|
-
]);
|
|
14
2
|
/**
|
|
15
3
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
16
4
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
17
5
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
18
|
-
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
19
6
|
*/
|
|
20
7
|
export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }) {
|
|
21
8
|
const firstSlot = slots[0];
|
|
@@ -120,61 +107,70 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
120
107
|
}
|
|
121
108
|
selectSlot(nextSlot(slot));
|
|
122
109
|
}, [clearSlot, isValidDate, lastSlot, nextSlot, selectSlot, slots, tryToComplete]);
|
|
123
|
-
// Цифры сегмента храним отдельно от текста: по `01` в поле не отличить `1` от `0` + `1`.
|
|
124
|
-
const typedRef = useRef(null);
|
|
125
|
-
const resetTyped = useCallback(() => {
|
|
126
|
-
typedRef.current = null;
|
|
127
|
-
}, []);
|
|
128
110
|
const typeDigit = useCallback((slot, key) => {
|
|
111
|
+
const text = getSlotText(slot);
|
|
112
|
+
const numberValue = Number(text) || 0;
|
|
113
|
+
const digit = Number(key);
|
|
114
|
+
const slotValue = parseInt(numberValue.toString() + key, 10) || 0;
|
|
115
|
+
const valueLength = slotValue.toString().length;
|
|
129
116
|
const maxLength = slot.max.toString().length;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
const isTheLastInput = /^0+$/.test(text) && maxLength === 2 && digit === 0;
|
|
118
|
+
if (valueLength < maxLength) {
|
|
119
|
+
if (slotValue || slotValue >= slot.min) {
|
|
120
|
+
updateSlot(slot, slotValue);
|
|
121
|
+
if (isTheLastInput)
|
|
122
|
+
checkInputAndGoNext(slot);
|
|
123
|
+
}
|
|
124
|
+
if (slotValue * 10 > slot.max) {
|
|
125
|
+
checkInputAndGoNext(slot);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (valueLength > maxLength) {
|
|
129
|
+
if (digit * 10 > slot.max) {
|
|
130
|
+
updateSlot(slot, key);
|
|
131
|
+
checkInputAndGoNext(slot);
|
|
132
|
+
}
|
|
133
|
+
else if (digit || digit >= slot.min) {
|
|
134
|
+
updateSlot(slot, key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (slotValue <= slot.max) {
|
|
138
|
+
updateSlot(slot, slotValue);
|
|
139
|
+
checkInputAndGoNext(slot);
|
|
143
140
|
}
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
else if (digit * 10 > slot.max) {
|
|
142
|
+
updateSlot(slot, key);
|
|
146
143
|
checkInputAndGoNext(slot);
|
|
147
|
-
|
|
144
|
+
}
|
|
145
|
+
else if (digit || digit >= slot.min) {
|
|
146
|
+
updateSlot(slot, key);
|
|
147
|
+
}
|
|
148
|
+
}, [checkInputAndGoNext, getSlotText, updateSlot]);
|
|
148
149
|
const handleFocus = useCallback(() => {
|
|
149
150
|
if (readonly || disabled)
|
|
150
151
|
return;
|
|
151
|
-
resetTyped();
|
|
152
152
|
ensureMask();
|
|
153
153
|
// На фокус всегда выбираем ПЕРВЫЙ сегмент (паритет с легаси). При клике мышью `handleClick`
|
|
154
154
|
// отработает следом и перевыберет сегмент под кареткой; при Tab остаётся первый.
|
|
155
155
|
selectSlot(firstSlot);
|
|
156
|
-
}, [disabled, ensureMask, firstSlot, readonly,
|
|
156
|
+
}, [disabled, ensureMask, firstSlot, readonly, selectSlot]);
|
|
157
157
|
const handleClick = useCallback((event) => {
|
|
158
158
|
if (readonly || disabled)
|
|
159
159
|
return;
|
|
160
|
-
resetTyped();
|
|
161
160
|
ensureMask();
|
|
162
161
|
selectSlot(slotFromIndex(event.currentTarget.selectionStart));
|
|
163
|
-
}, [disabled, ensureMask, readonly,
|
|
162
|
+
}, [disabled, ensureMask, readonly, selectSlot, slotFromIndex]);
|
|
164
163
|
const handleBlur = useCallback(() => {
|
|
165
|
-
resetTyped();
|
|
166
164
|
const input = inputRef.current;
|
|
167
165
|
if (input && input.value === mask) {
|
|
168
166
|
input.value = '';
|
|
169
167
|
setValue('');
|
|
170
168
|
}
|
|
171
|
-
}, [inputRef, mask,
|
|
169
|
+
}, [inputRef, mask, setValue]);
|
|
172
170
|
const handleKeyDown = useCallback((event) => {
|
|
173
171
|
const input = inputRef.current;
|
|
174
172
|
if (!input || readonly || disabled)
|
|
175
173
|
return;
|
|
176
|
-
if (RESET_TYPED_KEYS.has(event.key))
|
|
177
|
-
resetTyped();
|
|
178
174
|
// Tab уводит фокус из поля — не перехватываем.
|
|
179
175
|
if (event.key === 'Tab')
|
|
180
176
|
return;
|
|
@@ -189,13 +185,7 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
189
185
|
onArrowUp?.();
|
|
190
186
|
return;
|
|
191
187
|
}
|
|
192
|
-
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
193
|
-
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
194
|
-
return;
|
|
195
188
|
onEdit?.();
|
|
196
|
-
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
197
|
-
if (/^\d$/.test(event.key) || event.key === 'Unidentified')
|
|
198
|
-
return;
|
|
199
189
|
if (event.key === 'Escape') {
|
|
200
190
|
event.preventDefault();
|
|
201
191
|
onEscape?.();
|
|
@@ -230,6 +220,11 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
230
220
|
if (event.key === 'Enter') {
|
|
231
221
|
tryToComplete();
|
|
232
222
|
emit();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (/^\d$/.test(event.key)) {
|
|
226
|
+
typeDigit(slot, event.key);
|
|
227
|
+
emit();
|
|
233
228
|
}
|
|
234
229
|
}, [
|
|
235
230
|
clearSlot,
|
|
@@ -245,13 +240,21 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
245
240
|
onEscape,
|
|
246
241
|
prevSlot,
|
|
247
242
|
readonly,
|
|
248
|
-
resetTyped,
|
|
249
243
|
selectSlot,
|
|
250
244
|
slotFromIndex,
|
|
251
245
|
tryToComplete,
|
|
246
|
+
typeDigit,
|
|
252
247
|
]);
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
// Вставка (Ctrl+V) полной строки даты/времени: вытаскиваем цифры и заполняем сегменты
|
|
249
|
+
// слева направо. Нативный onChange у поля игнорируется (движок controlled), поэтому без
|
|
250
|
+
// этого обработчика вставка не работала бы вовсе.
|
|
251
|
+
const handlePaste = useCallback((event) => {
|
|
252
|
+
if (readonly || disabled)
|
|
253
|
+
return;
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
256
|
+
if (!digits)
|
|
257
|
+
return;
|
|
255
258
|
ensureMask();
|
|
256
259
|
let cursor = 0;
|
|
257
260
|
let lastFilled;
|
|
@@ -270,100 +273,7 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
270
273
|
else if (lastFilled) {
|
|
271
274
|
selectSlot(nextSlot(lastFilled));
|
|
272
275
|
}
|
|
273
|
-
}, [ensureMask, lastSlot, nextSlot, resetTyped, selectSlot, slots, tryToComplete, updateSlot]);
|
|
274
|
-
const handlePaste = useCallback((event) => {
|
|
275
|
-
if (readonly || disabled)
|
|
276
|
-
return;
|
|
277
|
-
event.preventDefault();
|
|
278
|
-
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
279
|
-
if (!digits)
|
|
280
|
-
return;
|
|
281
|
-
fillFromDigits(digits);
|
|
282
|
-
emit();
|
|
283
|
-
}, [disabled, emit, fillFromDigits, readonly]);
|
|
284
|
-
const applyEdit = useCallback((inputType, text) => {
|
|
285
|
-
const input = inputRef.current;
|
|
286
|
-
if (!input)
|
|
287
|
-
return;
|
|
288
|
-
onEdit?.();
|
|
289
|
-
if (inputType.startsWith('delete')) {
|
|
290
|
-
resetTyped();
|
|
291
|
-
clearSlot(slotFromIndex(input.selectionStart));
|
|
292
|
-
emit();
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const digits = text.replace(/\D/g, '');
|
|
296
|
-
if (!inputType.startsWith('insert') || !digits)
|
|
297
|
-
return;
|
|
298
|
-
// Автозаполнение, `fill` и drop присылают строку целиком.
|
|
299
|
-
if (text.length > 1) {
|
|
300
|
-
fillFromDigits(digits);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
typeDigit(slotFromIndex(input.selectionStart), digits);
|
|
304
|
-
}
|
|
305
|
-
emit();
|
|
306
|
-
}, [clearSlot, emit, fillFromDigits, inputRef, onEdit, resetTyped, slotFromIndex, typeDigit]);
|
|
307
|
-
// Композицию IME браузер не даёт отменить — откатываем её в input.
|
|
308
|
-
const pendingEditRef = useRef(null);
|
|
309
|
-
const handleBeforeInput = useCallback((event) => {
|
|
310
|
-
const input = inputRef.current;
|
|
311
|
-
if (!input || readonly || disabled)
|
|
312
|
-
return;
|
|
313
|
-
ensureMask();
|
|
314
|
-
if (!event.cancelable) {
|
|
315
|
-
pendingEditRef.current = { value: input.value, start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 };
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
event.preventDefault();
|
|
319
|
-
applyEdit(event.inputType, event.data ?? event.dataTransfer?.getData('text') ?? '');
|
|
320
|
-
}, [applyEdit, disabled, ensureMask, inputRef, readonly]);
|
|
321
|
-
const handleInput = useCallback((event) => {
|
|
322
|
-
const input = inputRef.current;
|
|
323
|
-
if (!input || readonly || disabled)
|
|
324
|
-
return;
|
|
325
|
-
const pending = pendingEditRef.current;
|
|
326
|
-
pendingEditRef.current = null;
|
|
327
|
-
if (pending) {
|
|
328
|
-
const inputType = event instanceof InputEvent ? event.inputType : '';
|
|
329
|
-
const inserted = input.value.slice(pending.start, input.value.length - (pending.value.length - pending.end));
|
|
330
|
-
input.value = pending.value;
|
|
331
|
-
setValue(pending.value);
|
|
332
|
-
input.setSelectionRange(pending.start, pending.end);
|
|
333
|
-
// Цифру композиции вводит её фиксация (insertText), иначе цифра запишется дважды.
|
|
334
|
-
if (!inputType.includes('Composition'))
|
|
335
|
-
applyEdit(inputType, inserted);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
// Автозаполнение меняет значение без beforeinput.
|
|
339
|
-
const digits = input.value.replace(/\D/g, '');
|
|
340
|
-
input.value = mask;
|
|
341
|
-
setValue(mask);
|
|
342
|
-
fillFromDigits(digits);
|
|
343
276
|
emit();
|
|
344
|
-
}, [
|
|
345
|
-
|
|
346
|
-
const onNativeBeforeInput = useEventHandler(handleBeforeInput);
|
|
347
|
-
const onNativeInput = useEventHandler(handleInput);
|
|
348
|
-
const attachedInputRef = useRef(null);
|
|
349
|
-
const nativeInputRef = useCallback((node) => {
|
|
350
|
-
/* eslint-disable @cloud-ru/ssr-safe-react/domApi -- callback-ref вызывается только в браузере */
|
|
351
|
-
attachedInputRef.current?.removeEventListener('beforeinput', onNativeBeforeInput);
|
|
352
|
-
attachedInputRef.current?.removeEventListener('input', onNativeInput);
|
|
353
|
-
attachedInputRef.current = node;
|
|
354
|
-
node?.addEventListener('beforeinput', onNativeBeforeInput);
|
|
355
|
-
node?.addEventListener('input', onNativeInput);
|
|
356
|
-
/* eslint-enable @cloud-ru/ssr-safe-react/domApi */
|
|
357
|
-
}, [onNativeBeforeInput, onNativeInput]);
|
|
358
|
-
return {
|
|
359
|
-
handleKeyDown,
|
|
360
|
-
handleClick,
|
|
361
|
-
handleFocus,
|
|
362
|
-
handleBlur,
|
|
363
|
-
handlePaste,
|
|
364
|
-
nativeInputRef,
|
|
365
|
-
selectSlot,
|
|
366
|
-
firstSlot,
|
|
367
|
-
lastSlot,
|
|
368
|
-
};
|
|
277
|
+
}, [disabled, emit, ensureMask, lastSlot, nextSlot, readonly, selectSlot, slots, tryToComplete, updateSlot]);
|
|
278
|
+
return { handleKeyDown, handleClick, handleFocus, handleBlur, handlePaste, selectSlot, firstSlot, lastSlot };
|
|
369
279
|
}
|