Source:

carville-shop

Демо:

Плагины:

Подключение:

//

Разметка:

<div class="input-count" data-min="1" data-max="100">
  <button type="button" data-step="-1">
    <svg
      width="16"
      height="16"
      aria-hidden="true"
      class="icon"
    >
      <use xlink:href="#counter-minus"></use>
    </svg>
  </button>
  <input type="number" value="1" style="width: 1ch" />
  <button type="button" data-step="1">
    <svg
      width="16"
      height="16"
      aria-hidden="true"
      class="icon"
    >
      <use xlink:href="#counter-plus"></use>
    </svg>
  </button>
</div>

Стили:

//

.input-count {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.2rem;
  border-radius: 3rem;
  background: #2c2d2e;

  button {
    padding: 1rem;
    color: #fff;

    .icon {
      font-size: 1.6rem;
    }
  }

  input {
    min-width: 3.2rem;
    text-align: center;
    font-size: 1.4rem;
    font-weight: 600;
    line-height: 1.6rem; /* 114.286% */
    color: #fff;
  }
}

Инициализация:

//

export default function initInputCounter(): void {
  document
    .querySelectorAll<HTMLElement>(".input-count")
    .forEach((el) => new InputCounter(el));
}


//
class InputCounter {
  private rootEl: HTMLElement;
  private inputEl: HTMLInputElement;
  private min: number;
  private max: number;

  constructor(rootEl: HTMLElement) {
    if (!rootEl) {
      throw new Error("InputCounter: rootEl is required");
    }

    const inputEl = rootEl.querySelector<HTMLInputElement>("input");
    if (!inputEl) {
      throw new Error("InputCounter: <input> element not found");
    }

    this.rootEl = rootEl;
    this.inputEl = inputEl;

    this.min = Number(rootEl.dataset.min) || 0;
    this.max = Number(rootEl.dataset.max) || Infinity;

    // Проверяем, есть ли уже созданный экземпляр
    const existing = (rootEl as any)._inputCounterInstance as InputCounter;
    if (existing) return existing;

    this.init();

    // Сохраняем экземпляр в DOM
    (this.rootEl as any)._inputCounterInstance = this;
  }

  private init(): void {
    this.rootEl.addEventListener("click", this.handleButtonClick);
    this.rootEl.addEventListener("input", this.handleInputChange);
    this.updateValue(Number(this.inputEl.value));
  }

  private handleButtonClick = (event: MouseEvent): void => {
    const target = event.target as HTMLElement | null;
    const btn = target?.closest<HTMLButtonElement>("button");
    if (!btn) return;

    const step = Number(btn.dataset.step);
    this.updateValue(Number(this.inputEl.value) + step);
  };

  private handleInputChange = (): void => {
    this.updateValue(Number(this.inputEl.value) || this.min);
  };

  private updateValue(newValue: number): void {
    const clamped = Math.min(this.max, Math.max(this.min, newValue));
    this.inputEl.value = String(clamped);
    this.inputEl.style.width = `${String(clamped).length}ch`;
  }

  public set(value: number): void {
    this.updateValue(value);
  }

  /** Получить экземпляр по DOM-элементу */
  public static getInstance(rootEl: HTMLElement): InputCounter | undefined {
    return (rootEl as any)._inputCounterInstance as InputCounter | undefined;
  }

  public destroy(): void {
    this.rootEl.removeEventListener("click", this.handleButtonClick);
    this.rootEl.removeEventListener("input", this.handleInputChange);
    delete (this.rootEl as any)._inputCounterInstance;
  }
}

export default InputCounter;

0 комментариев

Добавить комментарий

Avatar placeholder

Ваш адрес email не будет опубликован. Обязательные поля помечены *