diff --git a/src/Playground.vue b/src/Playground.vue index 29af7d22175783736ee8c1d760448460006f1316..31f397a2d64b605753a4508ee4611aa38ceea82c 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -23,6 +23,7 @@ import HomeIcon from '@icons/Mono/HomeIcon.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; import Carousel from '@components/Carousel/Carousel.vue'; import Toast from '@components/Toast/Toast.vue'; +import InputDiv from '@components/InputDiv/InputDiv.vue'; const visibleDrawer = ref(false); const sliderOptions: ISliderOptions[] = [ @@ -184,7 +185,6 @@ const openDrawer = () => (visibleDrawer.value = true); <template> <h2 class="title gradient-text">Playground</h2> - <input type="text" style="border: 1px solid black" /> <Button theme="black" label="Open all toasts" @@ -197,6 +197,7 @@ const openDrawer = () => (visibleDrawer.value = true); } " /> + <InputDiv :regex="/^[0-9-+=]+$/" /> <Button label="Open toast" @click="toast2 = true" /> <Toast v-model="toast" static :duration="60" type="success" position="topLeft" width="500px" /> <Toast v-model="toast4" :duration="2" type="info" position="topLeft" width="500px" /> diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index f6b1b0debb5f0522690669edb58d9b4719d81a23..f9480158568f02a991d690c62a27765963d82d27 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -227,9 +227,12 @@ export interface ISelectProps { export interface IInputDivProps { scheme?: TInputDivScheme; size?: TSize; + gap?: string; + inputsGap?: string; secret?: boolean; dashed?: boolean; numbersOnly?: boolean; + regex?: RegExp; bottomOnly?: boolean; theme?: TThemeColor; darknessTheme?: TDarkness; diff --git a/src/components/InputDiv/InputDiv.stories.ts b/src/components/InputDiv/InputDiv.stories.ts index 2b13d87d8c789d331a77ff0d0b1060d10c6a63ee..3c37e8e887561cc25c1d8a41746bf7f5a461fe27 100644 --- a/src/components/InputDiv/InputDiv.stories.ts +++ b/src/components/InputDiv/InputDiv.stories.ts @@ -20,6 +20,9 @@ const meta: Meta = { numbersOnly: { control: 'boolean' }, bottomOnly: { control: 'boolean' }, scheme: { control: 'text' }, + regex: { control: 'text' }, + gap: { control: 'text' }, + inputsGap: { control: 'text' }, darknessTheme: { control: 'select', options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] }, darknessTextColor: { control: 'select', @@ -76,3 +79,28 @@ type Story = StoryObj<typeof meta>; export const Simple: Story = { args: {}, }; + +export const Full: Story = { + args: { + secret: true, + dashed: true, + numbersOnly: true, + bottomOnly: false, + scheme: '2-4-3-1', + size: 'large', + theme: 'sky', + }, +}; + +export const Full2: Story = { + args: { + secret: false, + dashed: true, + numbersOnly: false, + bottomOnly: true, + scheme: '4by3', + size: 'large', + theme: 'white', + textColor: 'blue', + }, +}; diff --git a/src/components/InputDiv/InputDiv.vue b/src/components/InputDiv/InputDiv.vue index 61aaafd93eecd26995cd14da062368602d2b2f58..cce4d0598aeefc12acf3fc8387c3066ed7fdc2e9 100644 --- a/src/components/InputDiv/InputDiv.vue +++ b/src/components/InputDiv/InputDiv.vue @@ -2,7 +2,13 @@ import type { IInputDivProps } from '@interfaces/componentsProps'; import { computed, ref, type Ref, watch } from 'vue'; import { convertThemeToColor, convertThemeToTextColor, getValueFromSize } from '@helpers/common'; -import { calcPartsBy, calcPartsDash, changeInputHandler, moveFocus } from '@components/InputDiv/helpers'; +import { + calcIndexesToValueindex, + calcPartsBy, + calcPartsDash, + changeInputHandler, + moveFocus, +} from '@components/InputDiv/helpers'; const props = withDefaults(defineProps<IInputDivProps>(), { scheme: '4by1', @@ -15,39 +21,20 @@ const props = withDefaults(defineProps<IInputDivProps>(), { const value = defineModel() as Ref<string>; const valueParts = ref<string[]>([]); -watch(valueParts, () => { - value.value = valueParts.value.join(''); -}); +watch( + valueParts, + () => { + value.value = valueParts.value.join(''); + }, + { deep: true }, +); let container: HTMLElement | null; setTimeout(() => (container = document.querySelector('#inputDiv-container')), 0); const inputPartsBy = computed(() => calcPartsBy(props.scheme)); +const isInputPartsBy = computed(() => !!inputPartsBy.value); const inputPartsDash = computed(() => calcPartsDash(props.scheme)); -const indexesToValueIndex = computed(() => { - const result = {}; - let index = 0; - if (inputPartsBy.value) { - const splat = props.scheme.split('by'); - for (const itemIndex of [...Array(+splat[0]).keys()]) { - for (const inputIndex of [...Array(+splat[1]).keys()]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-expect-error - result[itemIndex + '-' + inputIndex] = index++; - } - } - } else { - const splat = props.scheme.split('-').map((i) => +i); - for (const item of splat) { - for (const inputIndex of [...Array(item).keys()]) { - const itemIndex = splat.indexOf(item); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-expect-error - result[itemIndex + '-' + inputIndex] = index++; - } - } - } - return result; -}); +const indexesToValueIndex = computed(() => calcIndexesToValueindex(isInputPartsBy.value, props.scheme)); const themeColor = computed(() => convertThemeToColor(props.theme, props.darknessTheme)); const color = computed(() => @@ -58,49 +45,85 @@ const color = computed(() => const inputWidth = computed(() => getValueFromSize(props.size, ['20px', '24px', '30px', '45px'])); const inputHeight = computed(() => getValueFromSize(props.size, ['30px', '36px', '45px', '67px'])); const fontSize = computed(() => getValueFromSize(props.size, ['12px', '16px', '24px', '32px'])); +const gap = computed(() => props.gap ?? fontSize.value); +const dashRight = computed(() => +gap.value.slice(0, -2) * -0.5 - 5 + 'px'); const borderWidth = computed(() => (props.size === 'small' || props.size === 'normal' ? '1px' : '2px')); const toggleInput = (target: any, itemIndex: number, inputIndex: number, backspace?: boolean) => (valueParts.value = changeInputHandler( target, container!, - !!inputPartsBy.value, + isInputPartsBy.value, valueParts.value, indexesToValueIndex.value, itemIndex, inputIndex, backspace ?? false, + props.numbersOnly, + props.regex ?? null, )); </script> <template> <section id="inputDiv-container"> <div v-show="inputPartsBy" class="list"> - <div v-for="(item, itemIndex) of inputPartsBy" :key="itemIndex" :class="`item ${itemIndex}`"> + <div + v-for="(item, itemIndex) of inputPartsBy" + :key="itemIndex" + :class="[ + `item ${itemIndex}`, + { + dashed: dashed && (inputPartsBy?.length ?? -1) - 1 !== itemIndex, + }, + ]" + > <input v-for="(_, inputIndex) of item" :key="inputIndex" @input="toggleInput($event.target, itemIndex, +inputIndex)" @keydown.delete="toggleInput($event.target, itemIndex, +inputIndex, true)" - @keydown.left="moveFocus('left', container!, !!inputPartsBy, itemIndex, inputIndex)" - @keydown.right="moveFocus('right', container!, !!inputPartsBy, itemIndex, inputIndex)" - type="text" - :class="`input ${inputIndex}`" + @keydown.left="moveFocus('left', container!, isInputPartsBy, itemIndex, inputIndex)" + @keydown.right="moveFocus('right', container!, isInputPartsBy, itemIndex, inputIndex)" + :type="secret ? 'password' : 'text'" + :class="[ + `input ${inputIndex}`, + { + firstInput: !bottomOnly && inputIndex === 0, + lastInput: !bottomOnly && inputPartsBy && inputIndex === inputPartsBy[itemIndex].length - 1, + bottomOnly, + }, + ]" maxlength="2" /> </div> </div> <div v-show="inputPartsDash" class="list"> - <div v-for="(item, itemIndex) of inputPartsDash" :key="itemIndex" :class="`item ${itemIndex}`"> + <div + v-for="(item, itemIndex) of inputPartsDash" + :key="itemIndex" + :class="[ + `item ${itemIndex}`, + { + dashed: dashed && (inputPartsDash?.length ?? -1) - 1 !== itemIndex, + }, + ]" + > <input v-for="(_, inputIndex) of item" :key="inputIndex" @input="toggleInput($event.target, itemIndex, +inputIndex)" @keydown.delete="toggleInput($event.target, itemIndex, +inputIndex, true)" - @keydown.left="moveFocus('left', container!, !!inputPartsBy, itemIndex, inputIndex)" - @keydown.right="moveFocus('right', container!, !!inputPartsBy, itemIndex, inputIndex)" - type="text" - :class="`input ${inputIndex}`" + @keydown.left="moveFocus('left', container!, isInputPartsBy, itemIndex, inputIndex)" + @keydown.right="moveFocus('right', container!, isInputPartsBy, itemIndex, inputIndex)" + :type="secret ? 'password' : 'text'" + :class="[ + `input ${inputIndex}`, + { + firstInput: !bottomOnly && inputIndex === 0, + lastInput: !bottomOnly && inputPartsDash && inputIndex === inputPartsDash[itemIndex].length - 1, + bottomOnly, + }, + ]" maxlength="2" /> </div> @@ -111,7 +134,7 @@ const toggleInput = (target: any, itemIndex: number, inputIndex: number, backspa <style scoped> .list { display: flex; - gap: v-bind(fontSize); + gap: v-bind(gap); } .input { all: unset; @@ -121,7 +144,39 @@ const toggleInput = (target: any, itemIndex: number, inputIndex: number, backspa text-align: center; background-color: v-bind(themeColor); color: v-bind(color); - border: v-bind(borderWidth) solid black; - border-radius: 5px; + border-top: v-bind(borderWidth) solid v-bind(color); + border-bottom: v-bind(borderWidth) solid v-bind(color); + border-right: v-bind(borderWidth) solid v-bind(color); +} +.input.bottomOnly { + border: none; + border-bottom: v-bind(borderWidth) solid v-bind(color); +} +.item { + position: relative; + display: flex; + gap: v-bind(inputsGap); +} +.item.dashed::after { + position: absolute; + color: v-bind(color); + z-index: 2; + top: calc(50% - 2px); + right: v-bind(dashRight); + content: '-'; + width: 10px; + height: 4px; + text-align: center; + line-height: 0; + font-size: v-bind(fontSize); +} +.firstInput { + border-left: v-bind(borderWidth) solid v-bind(color); + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} +.lastInput { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; } </style> diff --git a/src/components/InputDiv/helpers.ts b/src/components/InputDiv/helpers.ts index ab51752bd91fc6899cefa4b8c4c5cbde454b56be..4ece187e8d457c023f6c5788e4db58627da2333c 100644 --- a/src/components/InputDiv/helpers.ts +++ b/src/components/InputDiv/helpers.ts @@ -9,6 +9,8 @@ export const changeInputHandler = ( itemIndex: number, inputIndex: number, backspace: boolean, + numbersOnly: boolean, + regex: RegExp | null, ) => { let currentInput: HTMLInputElement | null = null; let currentItem: HTMLElement | null = null; @@ -27,15 +29,26 @@ export const changeInputHandler = ( } } } + + const valueIndex = indexesToValueIndex[(itemIndex + '-' + inputIndex) as keyof typeof indexesToValueIndex] as number; // еÑли значение ввели - if (currentInput?.value && currentItem) { - const valueIndex = indexesToValueIndex[ - (itemIndex + '-' + inputIndex) as keyof typeof indexesToValueIndex - ] as number; + if (currentInput?.value && currentItem && !backspace) { + const prevIndexValue = valueParts[valueIndex]; if (target.value.length === 2) { - currentInput!.value = target.value[0] === valueParts[valueIndex] ? target.value[1] : target.value[0]; + currentInput!.value = target.value[0] === prevIndexValue ? target.value[1] : target.value[0]; + } + if (numbersOnly && !currentInput!.value.match(/[0-9-]/)) { + if (!valueParts[valueIndex]) { + currentInput!.value = ''; + } + return valueParts; } valueParts[valueIndex] = currentInput!.value; + if (regex && !regex.test(valueParts.join(''))) { + currentInput!.value = ''; + valueParts[valueIndex] = prevIndexValue ?? ''; + return valueParts; + } // поиÑк Ñледующего инпута в той же чаÑти (еÑли еÑÑ‚ÑŒ) let nextInputInSameItem: HTMLInputElement | null = null; for (const child of currentItem.children) { @@ -54,7 +67,7 @@ export const changeInputHandler = ( targetInput.focus(); } } - } else if (backspace && currentItem) { + } else if (backspace && currentItem && currentInput) { // еÑли значение удалили let prevInputInSameItem: HTMLInputElement | null = null; for (const child of currentItem.children) { @@ -63,15 +76,28 @@ export const changeInputHandler = ( break; } } - if (prevInputInSameItem) { - prevInputInSameItem.focus(); - } else { + + let deletedCurrentValue = false; + if (currentInput.value) { + valueParts[valueIndex] = ''; + currentInput.value = ''; + deletedCurrentValue = true; + } else if (prevInputInSameItem) { + valueParts[valueIndex - 1] = ''; + setTimeout(() => prevInputInSameItem.focus(), 0); + prevInputInSameItem.value = ''; + } + if (!prevInputInSameItem) { // обработка предыдущей чаÑти, еÑли она еÑÑ‚ÑŒ, иначе ничего не делать - currentItem = list?.[currentItemIndex! - 1] as HTMLElement | null; - if (currentItem) { - const children = Array.from(currentItem.children); + const prevItem = list?.[currentItemIndex! - 1] as HTMLElement | null; + if (prevItem) { + const children = Array.from(prevItem.children); const targetInput = children[children.length - 1] as HTMLInputElement; - targetInput.focus(); + setTimeout(() => targetInput.focus(), 0); + if (!deletedCurrentValue) { + targetInput.value = ''; + valueParts[valueIndex - 1] = ''; + } } } } @@ -131,6 +157,32 @@ export const moveFocus = ( } }; +export const calcIndexesToValueindex = (inputPartsBy: boolean, scheme: TInputDivScheme) => { + const result = {}; + let index = 0; + if (inputPartsBy) { + const splat = scheme.split('by'); + for (const itemIndex of [...Array(+splat[0]).keys()]) { + for (const inputIndex of [...Array(+splat[1]).keys()]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-expect-error + result[itemIndex + '-' + inputIndex] = index++; + } + } + } else { + const splat = scheme.split('-').map((i: string) => +i); + for (const item of splat) { + for (const inputIndex of [...Array(item).keys()]) { + const itemIndex = splat.indexOf(item); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-expect-error + result[itemIndex + '-' + inputIndex] = index++; + } + } + } + return result; +}; + export const calcPartsBy = (scheme: TInputDivScheme) => { if (!scheme.includes('by')) return null; const splat = scheme.split('by');