Commit 00ef0e3d authored by Дмитрий Малюгин's avatar Дмитрий Малюгин 🕓
Browse files

feat: finish 'InputDiv'

parent 32001ced
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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" />
+3 −0
Original line number Diff line number Diff line
@@ -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;
+28 −0
Original line number Diff line number Diff line
@@ -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',
  },
};
+98 −43
Original line number Diff line number Diff line
@@ -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, () => {
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>
+65 −13
Original line number Diff line number Diff line
@@ -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');