@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
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import { useEventHandler } from '@cloud-ru/ds-utils';
|
|
2
1
|
import { ClipboardEvent, KeyboardEvent, MouseEvent, RefObject, useCallback, useRef } from 'react';
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const RESET_TYPED_KEYS = new Set([
|
|
7
|
-
'ArrowLeft',
|
|
8
|
-
'ArrowRight',
|
|
9
|
-
'ArrowUp',
|
|
10
|
-
'ArrowDown',
|
|
11
|
-
'Backspace',
|
|
12
|
-
'Delete',
|
|
13
|
-
'Enter',
|
|
14
|
-
'Tab',
|
|
15
|
-
'Escape',
|
|
16
|
-
]);
|
|
3
|
+
import { SlotMeta } from './segments';
|
|
17
4
|
|
|
18
5
|
type UseSegmentedMaskParams = {
|
|
19
6
|
inputRef: RefObject<HTMLInputElement | null>;
|
|
@@ -45,7 +32,6 @@ type UseSegmentedMaskParams = {
|
|
|
45
32
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
46
33
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
47
34
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
48
|
-
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
49
35
|
*/
|
|
50
36
|
export function useSegmentedMask({
|
|
51
37
|
inputRef,
|
|
@@ -188,72 +174,74 @@ export function useSegmentedMask({
|
|
|
188
174
|
[clearSlot, isValidDate, lastSlot, nextSlot, selectSlot, slots, tryToComplete],
|
|
189
175
|
);
|
|
190
176
|
|
|
191
|
-
// Цифры сегмента храним отдельно от текста: по `01` в поле не отличить `1` от `0` + `1`.
|
|
192
|
-
const typedRef = useRef<{ key: SegmentKey; digits: string } | null>(null);
|
|
193
|
-
const resetTyped = useCallback(() => {
|
|
194
|
-
typedRef.current = null;
|
|
195
|
-
}, []);
|
|
196
|
-
|
|
197
177
|
const typeDigit = useCallback(
|
|
198
178
|
(slot: SlotMeta, key: string) => {
|
|
179
|
+
const text = getSlotText(slot);
|
|
180
|
+
const numberValue = Number(text) || 0;
|
|
181
|
+
const digit = Number(key);
|
|
182
|
+
const slotValue = parseInt(numberValue.toString() + key, 10) || 0;
|
|
183
|
+
const valueLength = slotValue.toString().length;
|
|
199
184
|
const maxLength = slot.max.toString().length;
|
|
200
|
-
const
|
|
201
|
-
const appended = prev + key;
|
|
202
|
-
// Как в React Aria и MUI X: `13` в месяце → `03`.
|
|
203
|
-
const overflow = Number(appended) > slot.max;
|
|
204
|
-
const digits = overflow ? key : appended;
|
|
205
|
-
const value = Number(digits);
|
|
206
|
-
const complete = overflow || digits.length >= maxLength || value * 10 > slot.max;
|
|
207
|
-
const accepted = value >= slot.min;
|
|
208
|
-
|
|
209
|
-
if (accepted) updateSlot(slot, value);
|
|
210
|
-
|
|
211
|
-
if (!complete) {
|
|
212
|
-
typedRef.current = { key: slot.key, digits };
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
185
|
+
const isTheLastInput = /^0+$/.test(text) && maxLength === 2 && digit === 0;
|
|
215
186
|
|
|
216
|
-
|
|
217
|
-
|
|
187
|
+
if (valueLength < maxLength) {
|
|
188
|
+
if (slotValue || slotValue >= slot.min) {
|
|
189
|
+
updateSlot(slot, slotValue);
|
|
190
|
+
if (isTheLastInput) checkInputAndGoNext(slot);
|
|
191
|
+
}
|
|
192
|
+
if (slotValue * 10 > slot.max) {
|
|
193
|
+
checkInputAndGoNext(slot);
|
|
194
|
+
}
|
|
195
|
+
} else if (valueLength > maxLength) {
|
|
196
|
+
if (digit * 10 > slot.max) {
|
|
197
|
+
updateSlot(slot, key);
|
|
198
|
+
checkInputAndGoNext(slot);
|
|
199
|
+
} else if (digit || digit >= slot.min) {
|
|
200
|
+
updateSlot(slot, key);
|
|
201
|
+
}
|
|
202
|
+
} else if (slotValue <= slot.max) {
|
|
203
|
+
updateSlot(slot, slotValue);
|
|
204
|
+
checkInputAndGoNext(slot);
|
|
205
|
+
} else if (digit * 10 > slot.max) {
|
|
206
|
+
updateSlot(slot, key);
|
|
207
|
+
checkInputAndGoNext(slot);
|
|
208
|
+
} else if (digit || digit >= slot.min) {
|
|
209
|
+
updateSlot(slot, key);
|
|
210
|
+
}
|
|
218
211
|
},
|
|
219
|
-
[checkInputAndGoNext, updateSlot],
|
|
212
|
+
[checkInputAndGoNext, getSlotText, updateSlot],
|
|
220
213
|
);
|
|
221
214
|
|
|
222
215
|
const handleFocus = useCallback(() => {
|
|
223
216
|
if (readonly || disabled) return;
|
|
224
|
-
resetTyped();
|
|
225
217
|
ensureMask();
|
|
226
218
|
// На фокус всегда выбираем ПЕРВЫЙ сегмент (паритет с легаси). При клике мышью `handleClick`
|
|
227
219
|
// отработает следом и перевыберет сегмент под кареткой; при Tab остаётся первый.
|
|
228
220
|
selectSlot(firstSlot);
|
|
229
|
-
}, [disabled, ensureMask, firstSlot, readonly,
|
|
221
|
+
}, [disabled, ensureMask, firstSlot, readonly, selectSlot]);
|
|
230
222
|
|
|
231
223
|
const handleClick = useCallback(
|
|
232
224
|
(event: MouseEvent<HTMLInputElement>) => {
|
|
233
225
|
if (readonly || disabled) return;
|
|
234
|
-
resetTyped();
|
|
235
226
|
ensureMask();
|
|
236
227
|
selectSlot(slotFromIndex(event.currentTarget.selectionStart));
|
|
237
228
|
},
|
|
238
|
-
[disabled, ensureMask, readonly,
|
|
229
|
+
[disabled, ensureMask, readonly, selectSlot, slotFromIndex],
|
|
239
230
|
);
|
|
240
231
|
|
|
241
232
|
const handleBlur = useCallback(() => {
|
|
242
|
-
resetTyped();
|
|
243
233
|
const input = inputRef.current;
|
|
244
234
|
if (input && input.value === mask) {
|
|
245
235
|
input.value = '';
|
|
246
236
|
setValue('');
|
|
247
237
|
}
|
|
248
|
-
}, [inputRef, mask,
|
|
238
|
+
}, [inputRef, mask, setValue]);
|
|
249
239
|
|
|
250
240
|
const handleKeyDown = useCallback(
|
|
251
241
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
|
252
242
|
const input = inputRef.current;
|
|
253
243
|
if (!input || readonly || disabled) return;
|
|
254
244
|
|
|
255
|
-
if (RESET_TYPED_KEYS.has(event.key)) resetTyped();
|
|
256
|
-
|
|
257
245
|
// Tab уводит фокус из поля — не перехватываем.
|
|
258
246
|
if (event.key === 'Tab') return;
|
|
259
247
|
|
|
@@ -270,14 +258,8 @@ export function useSegmentedMask({
|
|
|
270
258
|
return;
|
|
271
259
|
}
|
|
272
260
|
|
|
273
|
-
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
274
|
-
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
275
|
-
|
|
276
261
|
onEdit?.();
|
|
277
262
|
|
|
278
|
-
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
279
|
-
if (/^\d$/.test(event.key) || event.key === 'Unidentified') return;
|
|
280
|
-
|
|
281
263
|
if (event.key === 'Escape') {
|
|
282
264
|
event.preventDefault();
|
|
283
265
|
onEscape?.();
|
|
@@ -315,6 +297,11 @@ export function useSegmentedMask({
|
|
|
315
297
|
if (event.key === 'Enter') {
|
|
316
298
|
tryToComplete();
|
|
317
299
|
emit();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (/^\d$/.test(event.key)) {
|
|
303
|
+
typeDigit(slot, event.key);
|
|
304
|
+
emit();
|
|
318
305
|
}
|
|
319
306
|
},
|
|
320
307
|
[
|
|
@@ -331,16 +318,22 @@ export function useSegmentedMask({
|
|
|
331
318
|
onEscape,
|
|
332
319
|
prevSlot,
|
|
333
320
|
readonly,
|
|
334
|
-
resetTyped,
|
|
335
321
|
selectSlot,
|
|
336
322
|
slotFromIndex,
|
|
337
323
|
tryToComplete,
|
|
324
|
+
typeDigit,
|
|
338
325
|
],
|
|
339
326
|
);
|
|
340
327
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
328
|
+
// Вставка (Ctrl+V) полной строки даты/времени: вытаскиваем цифры и заполняем сегменты
|
|
329
|
+
// слева направо. Нативный onChange у поля игнорируется (движок controlled), поэтому без
|
|
330
|
+
// этого обработчика вставка не работала бы вовсе.
|
|
331
|
+
const handlePaste = useCallback(
|
|
332
|
+
(event: ClipboardEvent<HTMLInputElement>) => {
|
|
333
|
+
if (readonly || disabled) return;
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
336
|
+
if (!digits) return;
|
|
344
337
|
ensureMask();
|
|
345
338
|
|
|
346
339
|
let cursor = 0;
|
|
@@ -359,123 +352,10 @@ export function useSegmentedMask({
|
|
|
359
352
|
} else if (lastFilled) {
|
|
360
353
|
selectSlot(nextSlot(lastFilled));
|
|
361
354
|
}
|
|
362
|
-
},
|
|
363
|
-
[ensureMask, lastSlot, nextSlot, resetTyped, selectSlot, slots, tryToComplete, updateSlot],
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const handlePaste = useCallback(
|
|
367
|
-
(event: ClipboardEvent<HTMLInputElement>) => {
|
|
368
|
-
if (readonly || disabled) return;
|
|
369
|
-
event.preventDefault();
|
|
370
|
-
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
371
|
-
if (!digits) return;
|
|
372
|
-
fillFromDigits(digits);
|
|
373
|
-
emit();
|
|
374
|
-
},
|
|
375
|
-
[disabled, emit, fillFromDigits, readonly],
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
const applyEdit = useCallback(
|
|
379
|
-
(inputType: string, text: string) => {
|
|
380
|
-
const input = inputRef.current;
|
|
381
|
-
if (!input) return;
|
|
382
|
-
onEdit?.();
|
|
383
|
-
|
|
384
|
-
if (inputType.startsWith('delete')) {
|
|
385
|
-
resetTyped();
|
|
386
|
-
clearSlot(slotFromIndex(input.selectionStart));
|
|
387
|
-
emit();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const digits = text.replace(/\D/g, '');
|
|
392
|
-
if (!inputType.startsWith('insert') || !digits) return;
|
|
393
|
-
|
|
394
|
-
// Автозаполнение, `fill` и drop присылают строку целиком.
|
|
395
|
-
if (text.length > 1) {
|
|
396
|
-
fillFromDigits(digits);
|
|
397
|
-
} else {
|
|
398
|
-
typeDigit(slotFromIndex(input.selectionStart), digits);
|
|
399
|
-
}
|
|
400
355
|
emit();
|
|
401
356
|
},
|
|
402
|
-
[
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
// Композицию IME браузер не даёт отменить — откатываем её в input.
|
|
406
|
-
const pendingEditRef = useRef<{ value: string; start: number; end: number } | null>(null);
|
|
407
|
-
|
|
408
|
-
const handleBeforeInput = useCallback(
|
|
409
|
-
(event: InputEvent) => {
|
|
410
|
-
const input = inputRef.current;
|
|
411
|
-
if (!input || readonly || disabled) return;
|
|
412
|
-
ensureMask();
|
|
413
|
-
|
|
414
|
-
if (!event.cancelable) {
|
|
415
|
-
pendingEditRef.current = { value: input.value, start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 };
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
event.preventDefault();
|
|
420
|
-
applyEdit(event.inputType, event.data ?? event.dataTransfer?.getData('text') ?? '');
|
|
421
|
-
},
|
|
422
|
-
[applyEdit, disabled, ensureMask, inputRef, readonly],
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
const handleInput = useCallback(
|
|
426
|
-
(event: Event) => {
|
|
427
|
-
const input = inputRef.current;
|
|
428
|
-
if (!input || readonly || disabled) return;
|
|
429
|
-
const pending = pendingEditRef.current;
|
|
430
|
-
pendingEditRef.current = null;
|
|
431
|
-
|
|
432
|
-
if (pending) {
|
|
433
|
-
const inputType = event instanceof InputEvent ? event.inputType : '';
|
|
434
|
-
const inserted = input.value.slice(pending.start, input.value.length - (pending.value.length - pending.end));
|
|
435
|
-
input.value = pending.value;
|
|
436
|
-
setValue(pending.value);
|
|
437
|
-
input.setSelectionRange(pending.start, pending.end);
|
|
438
|
-
// Цифру композиции вводит её фиксация (insertText), иначе цифра запишется дважды.
|
|
439
|
-
if (!inputType.includes('Composition')) applyEdit(inputType, inserted);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Автозаполнение меняет значение без beforeinput.
|
|
444
|
-
const digits = input.value.replace(/\D/g, '');
|
|
445
|
-
input.value = mask;
|
|
446
|
-
setValue(mask);
|
|
447
|
-
fillFromDigits(digits);
|
|
448
|
-
emit();
|
|
449
|
-
},
|
|
450
|
-
[applyEdit, disabled, emit, fillFromDigits, inputRef, mask, readonly, setValue],
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
// React 18 не отдаёт `inputType` в `onBeforeInput`, поэтому слушаем нативные события.
|
|
454
|
-
const onNativeBeforeInput = useEventHandler(handleBeforeInput);
|
|
455
|
-
const onNativeInput = useEventHandler(handleInput);
|
|
456
|
-
const attachedInputRef = useRef<HTMLInputElement | null>(null);
|
|
457
|
-
const nativeInputRef = useCallback(
|
|
458
|
-
(node: HTMLInputElement | null) => {
|
|
459
|
-
/* eslint-disable @cloud-ru/ssr-safe-react/domApi -- callback-ref вызывается только в браузере */
|
|
460
|
-
attachedInputRef.current?.removeEventListener('beforeinput', onNativeBeforeInput);
|
|
461
|
-
attachedInputRef.current?.removeEventListener('input', onNativeInput);
|
|
462
|
-
attachedInputRef.current = node;
|
|
463
|
-
node?.addEventListener('beforeinput', onNativeBeforeInput);
|
|
464
|
-
node?.addEventListener('input', onNativeInput);
|
|
465
|
-
/* eslint-enable @cloud-ru/ssr-safe-react/domApi */
|
|
466
|
-
},
|
|
467
|
-
[onNativeBeforeInput, onNativeInput],
|
|
357
|
+
[disabled, emit, ensureMask, lastSlot, nextSlot, readonly, selectSlot, slots, tryToComplete, updateSlot],
|
|
468
358
|
);
|
|
469
359
|
|
|
470
|
-
return {
|
|
471
|
-
handleKeyDown,
|
|
472
|
-
handleClick,
|
|
473
|
-
handleFocus,
|
|
474
|
-
handleBlur,
|
|
475
|
-
handlePaste,
|
|
476
|
-
nativeInputRef,
|
|
477
|
-
selectSlot,
|
|
478
|
-
firstSlot,
|
|
479
|
-
lastSlot,
|
|
480
|
-
};
|
|
360
|
+
return { handleKeyDown, handleClick, handleFocus, handleBlur, handlePaste, selectSlot, firstSlot, lastSlot };
|
|
481
361
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Size } from '@cloud-ru/ds-field-decorator';
|
|
2
|
-
import { ItemId } from '@cloud-ru/ds-list';
|
|
3
|
-
import { Appearance as TagAppearance, Size as TagSize } from '@cloud-ru/ds-tag';
|
|
4
|
-
import { FieldSelectItem, FieldSelectMultipleProps, FieldSelectProps } from './types';
|
|
5
|
-
export declare const TAG_SIZE_MAP: Record<Size, TagSize>;
|
|
6
|
-
export type WithIdContent = {
|
|
7
|
-
id?: ItemId;
|
|
8
|
-
content?: unknown;
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
appearance?: TagAppearance;
|
|
11
|
-
};
|
|
12
|
-
export declare function extractLabel(item: WithIdContent): string;
|
|
13
|
-
export declare function extractSearchText(item: WithIdContent): string;
|
|
14
|
-
export declare function extractAppearance(item?: WithIdContent): TagAppearance | undefined;
|
|
15
|
-
export declare function flatten(items: FieldSelectItem[]): WithIdContent[];
|
|
16
|
-
export declare function findItem(items: FieldSelectItem[], id: ItemId): WithIdContent | undefined;
|
|
17
|
-
export declare function isFuzzyMatch(haystack: string, needle: string): boolean;
|
|
18
|
-
export declare function filterItems(items: FieldSelectItem[], query: string, fuzzy: boolean): FieldSelectItem[];
|
|
19
|
-
export declare function isMultiple(props: FieldSelectProps): props is FieldSelectMultipleProps;
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TAG_SIZE_MAP = void 0;
|
|
4
|
-
exports.extractLabel = extractLabel;
|
|
5
|
-
exports.extractSearchText = extractSearchText;
|
|
6
|
-
exports.extractAppearance = extractAppearance;
|
|
7
|
-
exports.flatten = flatten;
|
|
8
|
-
exports.findItem = findItem;
|
|
9
|
-
exports.isFuzzyMatch = isFuzzyMatch;
|
|
10
|
-
exports.filterItems = filterItems;
|
|
11
|
-
exports.isMultiple = isMultiple;
|
|
12
|
-
const constants_1 = require("./constants");
|
|
13
|
-
exports.TAG_SIZE_MAP = { s: 'xs', m: 'xs', l: 's' };
|
|
14
|
-
function extractLabel(item) {
|
|
15
|
-
const { content, id } = item;
|
|
16
|
-
if (!content) {
|
|
17
|
-
return String(id ?? '');
|
|
18
|
-
}
|
|
19
|
-
if (typeof content === 'string' || typeof content === 'number') {
|
|
20
|
-
return String(content);
|
|
21
|
-
}
|
|
22
|
-
if (typeof content === 'object' && content !== null && 'label' in content) {
|
|
23
|
-
return String(content.label);
|
|
24
|
-
}
|
|
25
|
-
return String(id ?? '');
|
|
26
|
-
}
|
|
27
|
-
// Текст для поиска: лейбл (option) + caption + description — паритет с легаси `useSearch`,
|
|
28
|
-
// который матчил запрос по всем трём полям контента, а не только по лейблу.
|
|
29
|
-
function extractSearchText(item) {
|
|
30
|
-
const parts = [extractLabel(item)];
|
|
31
|
-
const { content } = item;
|
|
32
|
-
if (content && typeof content === 'object') {
|
|
33
|
-
const c = content;
|
|
34
|
-
if (typeof c.caption === 'string') {
|
|
35
|
-
parts.push(c.caption);
|
|
36
|
-
}
|
|
37
|
-
if (typeof c.description === 'string') {
|
|
38
|
-
parts.push(c.description);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return parts.join(' ');
|
|
42
|
-
}
|
|
43
|
-
// Цвет тега выбранного значения (multiple) — паритет с легаси `option.appearance`.
|
|
44
|
-
// Проверка типа нужна и при типизированном поле: items часто приходят из ответа бэкенда.
|
|
45
|
-
function extractAppearance(item) {
|
|
46
|
-
return typeof item?.appearance === 'string' ? item.appearance : undefined;
|
|
47
|
-
}
|
|
48
|
-
function flatten(items) {
|
|
49
|
-
const out = [];
|
|
50
|
-
for (const item of items) {
|
|
51
|
-
out.push(item);
|
|
52
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
53
|
-
out.push(...flatten(item.items));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
function findItem(items, id) {
|
|
59
|
-
for (const item of flatten(items)) {
|
|
60
|
-
if (item.id === id) {
|
|
61
|
-
return item;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
// Subsequence-fuzzy: символы запроса должны встречаться в строке в исходном порядке.
|
|
67
|
-
// Пример: query=`lge` matches `Large` (L → r → arg → e).
|
|
68
|
-
function isFuzzyMatch(haystack, needle) {
|
|
69
|
-
if (!needle) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
let i = 0;
|
|
73
|
-
for (const ch of haystack) {
|
|
74
|
-
if (ch === needle[i]) {
|
|
75
|
-
i += 1;
|
|
76
|
-
if (i === needle.length) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
function filterItems(items, query, fuzzy) {
|
|
84
|
-
if (!query) {
|
|
85
|
-
return items;
|
|
86
|
-
}
|
|
87
|
-
const q = query.toLowerCase();
|
|
88
|
-
const matches = (item) => {
|
|
89
|
-
const text = extractSearchText(item).toLowerCase();
|
|
90
|
-
return fuzzy ? isFuzzyMatch(text, q) : text.includes(q);
|
|
91
|
-
};
|
|
92
|
-
const walk = (list) => list.flatMap(item => {
|
|
93
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
94
|
-
const filteredChildren = walk(item.items);
|
|
95
|
-
return filteredChildren.length > 0 ? [{ ...item, items: filteredChildren }] : [];
|
|
96
|
-
}
|
|
97
|
-
return matches(item) ? [item] : [];
|
|
98
|
-
});
|
|
99
|
-
return walk(items);
|
|
100
|
-
}
|
|
101
|
-
function isMultiple(props) {
|
|
102
|
-
return props.selection === constants_1.SELECTION_MODE.Multiple;
|
|
103
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Size } from '@cloud-ru/ds-field-decorator';
|
|
2
|
-
import { ItemId } from '@cloud-ru/ds-list';
|
|
3
|
-
import { Appearance as TagAppearance, Size as TagSize } from '@cloud-ru/ds-tag';
|
|
4
|
-
import { FieldSelectItem, FieldSelectMultipleProps, FieldSelectProps } from './types.js';
|
|
5
|
-
export declare const TAG_SIZE_MAP: Record<Size, TagSize>;
|
|
6
|
-
export type WithIdContent = {
|
|
7
|
-
id?: ItemId;
|
|
8
|
-
content?: unknown;
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
appearance?: TagAppearance;
|
|
11
|
-
};
|
|
12
|
-
export declare function extractLabel(item: WithIdContent): string;
|
|
13
|
-
export declare function extractSearchText(item: WithIdContent): string;
|
|
14
|
-
export declare function extractAppearance(item?: WithIdContent): TagAppearance | undefined;
|
|
15
|
-
export declare function flatten(items: FieldSelectItem[]): WithIdContent[];
|
|
16
|
-
export declare function findItem(items: FieldSelectItem[], id: ItemId): WithIdContent | undefined;
|
|
17
|
-
export declare function isFuzzyMatch(haystack: string, needle: string): boolean;
|
|
18
|
-
export declare function filterItems(items: FieldSelectItem[], query: string, fuzzy: boolean): FieldSelectItem[];
|
|
19
|
-
export declare function isMultiple(props: FieldSelectProps): props is FieldSelectMultipleProps;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { SELECTION_MODE } from './constants.js';
|
|
2
|
-
export const TAG_SIZE_MAP = { s: 'xs', m: 'xs', l: 's' };
|
|
3
|
-
export function extractLabel(item) {
|
|
4
|
-
const { content, id } = item;
|
|
5
|
-
if (!content) {
|
|
6
|
-
return String(id ?? '');
|
|
7
|
-
}
|
|
8
|
-
if (typeof content === 'string' || typeof content === 'number') {
|
|
9
|
-
return String(content);
|
|
10
|
-
}
|
|
11
|
-
if (typeof content === 'object' && content !== null && 'label' in content) {
|
|
12
|
-
return String(content.label);
|
|
13
|
-
}
|
|
14
|
-
return String(id ?? '');
|
|
15
|
-
}
|
|
16
|
-
// Текст для поиска: лейбл (option) + caption + description — паритет с легаси `useSearch`,
|
|
17
|
-
// который матчил запрос по всем трём полям контента, а не только по лейблу.
|
|
18
|
-
export function extractSearchText(item) {
|
|
19
|
-
const parts = [extractLabel(item)];
|
|
20
|
-
const { content } = item;
|
|
21
|
-
if (content && typeof content === 'object') {
|
|
22
|
-
const c = content;
|
|
23
|
-
if (typeof c.caption === 'string') {
|
|
24
|
-
parts.push(c.caption);
|
|
25
|
-
}
|
|
26
|
-
if (typeof c.description === 'string') {
|
|
27
|
-
parts.push(c.description);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return parts.join(' ');
|
|
31
|
-
}
|
|
32
|
-
// Цвет тега выбранного значения (multiple) — паритет с легаси `option.appearance`.
|
|
33
|
-
// Проверка типа нужна и при типизированном поле: items часто приходят из ответа бэкенда.
|
|
34
|
-
export function extractAppearance(item) {
|
|
35
|
-
return typeof item?.appearance === 'string' ? item.appearance : undefined;
|
|
36
|
-
}
|
|
37
|
-
export function flatten(items) {
|
|
38
|
-
const out = [];
|
|
39
|
-
for (const item of items) {
|
|
40
|
-
out.push(item);
|
|
41
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
42
|
-
out.push(...flatten(item.items));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return out;
|
|
46
|
-
}
|
|
47
|
-
export function findItem(items, id) {
|
|
48
|
-
for (const item of flatten(items)) {
|
|
49
|
-
if (item.id === id) {
|
|
50
|
-
return item;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
// Subsequence-fuzzy: символы запроса должны встречаться в строке в исходном порядке.
|
|
56
|
-
// Пример: query=`lge` matches `Large` (L → r → arg → e).
|
|
57
|
-
export function isFuzzyMatch(haystack, needle) {
|
|
58
|
-
if (!needle) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
let i = 0;
|
|
62
|
-
for (const ch of haystack) {
|
|
63
|
-
if (ch === needle[i]) {
|
|
64
|
-
i += 1;
|
|
65
|
-
if (i === needle.length) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
export function filterItems(items, query, fuzzy) {
|
|
73
|
-
if (!query) {
|
|
74
|
-
return items;
|
|
75
|
-
}
|
|
76
|
-
const q = query.toLowerCase();
|
|
77
|
-
const matches = (item) => {
|
|
78
|
-
const text = extractSearchText(item).toLowerCase();
|
|
79
|
-
return fuzzy ? isFuzzyMatch(text, q) : text.includes(q);
|
|
80
|
-
};
|
|
81
|
-
const walk = (list) => list.flatMap(item => {
|
|
82
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
83
|
-
const filteredChildren = walk(item.items);
|
|
84
|
-
return filteredChildren.length > 0 ? [{ ...item, items: filteredChildren }] : [];
|
|
85
|
-
}
|
|
86
|
-
return matches(item) ? [item] : [];
|
|
87
|
-
});
|
|
88
|
-
return walk(items);
|
|
89
|
-
}
|
|
90
|
-
export function isMultiple(props) {
|
|
91
|
-
return props.selection === SELECTION_MODE.Multiple;
|
|
92
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { Size } from '@cloud-ru/ds-field-decorator';
|
|
2
|
-
import { ItemId } from '@cloud-ru/ds-list';
|
|
3
|
-
import { Appearance as TagAppearance, Size as TagSize } from '@cloud-ru/ds-tag';
|
|
4
|
-
|
|
5
|
-
import { SELECTION_MODE } from './constants';
|
|
6
|
-
import { FieldSelectItem, FieldSelectMultipleProps, FieldSelectProps } from './types';
|
|
7
|
-
|
|
8
|
-
export const TAG_SIZE_MAP: Record<Size, TagSize> = { s: 'xs', m: 'xs', l: 's' };
|
|
9
|
-
|
|
10
|
-
export type WithIdContent = { id?: ItemId; content?: unknown; disabled?: boolean; appearance?: TagAppearance };
|
|
11
|
-
|
|
12
|
-
export function extractLabel(item: WithIdContent): string {
|
|
13
|
-
const { content, id } = item;
|
|
14
|
-
|
|
15
|
-
if (!content) {
|
|
16
|
-
return String(id ?? '');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (typeof content === 'string' || typeof content === 'number') {
|
|
20
|
-
return String(content);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (typeof content === 'object' && content !== null && 'label' in content) {
|
|
24
|
-
return String((content as { label: unknown }).label);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return String(id ?? '');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Текст для поиска: лейбл (option) + caption + description — паритет с легаси `useSearch`,
|
|
31
|
-
// который матчил запрос по всем трём полям контента, а не только по лейблу.
|
|
32
|
-
export function extractSearchText(item: WithIdContent): string {
|
|
33
|
-
const parts = [extractLabel(item)];
|
|
34
|
-
const { content } = item;
|
|
35
|
-
|
|
36
|
-
if (content && typeof content === 'object') {
|
|
37
|
-
const c = content as { caption?: unknown; description?: unknown };
|
|
38
|
-
|
|
39
|
-
if (typeof c.caption === 'string') {
|
|
40
|
-
parts.push(c.caption);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (typeof c.description === 'string') {
|
|
44
|
-
parts.push(c.description);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return parts.join(' ');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Цвет тега выбранного значения (multiple) — паритет с легаси `option.appearance`.
|
|
52
|
-
// Проверка типа нужна и при типизированном поле: items часто приходят из ответа бэкенда.
|
|
53
|
-
export function extractAppearance(item?: WithIdContent): TagAppearance | undefined {
|
|
54
|
-
return typeof item?.appearance === 'string' ? item.appearance : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function flatten(items: FieldSelectItem[]): WithIdContent[] {
|
|
58
|
-
const out: WithIdContent[] = [];
|
|
59
|
-
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
out.push(item as WithIdContent);
|
|
62
|
-
|
|
63
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
64
|
-
out.push(...flatten(item.items as FieldSelectItem[]));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function findItem(items: FieldSelectItem[], id: ItemId): WithIdContent | undefined {
|
|
72
|
-
for (const item of flatten(items)) {
|
|
73
|
-
if (item.id === id) {
|
|
74
|
-
return item;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Subsequence-fuzzy: символы запроса должны встречаться в строке в исходном порядке.
|
|
82
|
-
// Пример: query=`lge` matches `Large` (L → r → arg → e).
|
|
83
|
-
export function isFuzzyMatch(haystack: string, needle: string): boolean {
|
|
84
|
-
if (!needle) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let i = 0;
|
|
89
|
-
|
|
90
|
-
for (const ch of haystack) {
|
|
91
|
-
if (ch === needle[i]) {
|
|
92
|
-
i += 1;
|
|
93
|
-
|
|
94
|
-
if (i === needle.length) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function filterItems(items: FieldSelectItem[], query: string, fuzzy: boolean): FieldSelectItem[] {
|
|
104
|
-
if (!query) {
|
|
105
|
-
return items;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const q = query.toLowerCase();
|
|
109
|
-
|
|
110
|
-
const matches = (item: FieldSelectItem): boolean => {
|
|
111
|
-
const text = extractSearchText(item as WithIdContent).toLowerCase();
|
|
112
|
-
|
|
113
|
-
return fuzzy ? isFuzzyMatch(text, q) : text.includes(q);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const walk = (list: FieldSelectItem[]): FieldSelectItem[] =>
|
|
117
|
-
list.flatMap(item => {
|
|
118
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
119
|
-
const filteredChildren = walk(item.items as FieldSelectItem[]);
|
|
120
|
-
|
|
121
|
-
return filteredChildren.length > 0 ? [{ ...item, items: filteredChildren } as FieldSelectItem] : [];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return matches(item) ? [item] : [];
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
return walk(items);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function isMultiple(props: FieldSelectProps): props is FieldSelectMultipleProps {
|
|
131
|
-
return props.selection === SELECTION_MODE.Multiple;
|
|
132
|
-
}
|