npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brandup/ui-input

v1.0.58

Published

Input control type and styles.

Readme

@brandup/ui-input

Build Status

Общая база компонентов ввода в форме: абстрактные классы InputControl и EditorInputControl, а также LESS-миксин для поля-носителя значения. Пакет ничего не рендерит сам — им пользуются @brandup/ui-textbox, @brandup/ui-messageeditor и @brandup/ui-dropdown.

Установка

npm i @brandup/ui-input

Версии. @brandup/ui-textbox, @brandup/ui-messageeditor и @brandup/ui-dropdown подключают из этого пакета не только TypeScript, но и LESS (см. LESS). Пакеты кита версионируются одной сборкой CI и рассчитаны друг на друга: обновляя любой из них, обновляйте и @brandup/ui-input. Иначе сборка стилей падает ещё до TypeScript — на @import отсутствующего input.less либо на неизвестном миксине .ui-input-hidden-value.

InputControl

InputControl<T, TEvents> — абстрактный класс, от которого наследуются все компоненты ввода. Расширяет UIElementBound из @brandup/ui. Параметр T — тип поля-носителя: HTMLInputElement, HTMLTextAreaElement или HTMLSelectElement.

Свойства

| Свойство | Тип | Описание | | --- | --- | --- | | form | HTMLFormElement \| null | Форма, к которой привязано поле-носитель | | disabled | boolean | Поле отключено | | required | boolean | Поле обязательно для заполнения | | readonly | boolean | Поле только для чтения (readonly или data-readonly) | | autoFocus | boolean | Объявлен ли автофокус (autofocus или data-autofocus) |

Методы

| Метод | Описание | | --- | --- | | validate(): boolean | Синхронизирует значение и проверяет его нативным checkValidity() | | focus(scroll?): void | Ведёт фокус в контрол и прокручивает его в видимую область. scrollScrollLogicalPosition для scrollIntoView: по умолчанию "center" (фокус из кода обычно ведут к тому, что нужно показать), автофокус просит "nearest" | | destroy(): void | Снимает обработчики, возвращает поле-носитель в исходное состояние и удаляет контейнер контрола |

Поведение

  • Добавляет CSS-класс ui-input на корневой элемент, а также required, readonly, disabled — по состоянию поля. Имена — в объекте INPUT (@brandup/ui-input/names). Оформлены китом readonly, disabled и invalid; required только выставляется — обязательность показывают по-разному (звёздочка у подписи, пометка в сводке ошибок), и это остаётся за хостом.
  • Гасит показ нативной подсказки валидации (invalid): поле уведено с экрана, привязать подсказку не к чему. Само решение о валидности остаётся за браузером, а состояние видно по классу invalid.
  • Переносит значение в поле-носитель до отправки формы — и в фазе перехвата на документе, чтобы обработчик submit, повешенный приложением раньше контрола, тоже увидел актуальное значение.
  • focus() ничего не делает у выключенного контрола — как нативный disabled input, который игнорирует focus() сам. Поле только для чтения фокусируется: это его нативное поведение, текст читают, выделяют и копируют.

Автофокус

Поле объявляет его нативным autofocus либо data-autofocus. Браузер разбирает нативный атрибут при разборе разметки, когда контрола ещё нет: поле, скрытое классом уже в разметке, он пропустит, а видимое — сфокусирует, но перенос поля в контейнер контрола этот фокус тут же собьёт (браузер снимает фокус с перемещаемого узла). Так что фокус в любом случае ставит контрол, а атрибут остаётся объявлением намерения. Наследник зовёт __applyAutoFocus() в конце своего конструктора — раньше нельзя, ввод контрола ещё не собран.

Фокус ставится через тот же focus("nearest"): вид двигается на минимум — страницу ещё не читали, и сдвигать её ради поля, которое и так на виду, не за чем. Автофокус — пожелание разметки, а не команда, поэтому молча отменяется, если:

  • поле выключено или только для чтения;
  • вводят пальцем ((pointer: coarse)) — экранная клавиатура закрыла бы страницу, которую ещё не читали;
  • пользователь уже прокрутил страницу сам — вид принадлежит ему;
  • фокус держит поле ввода или другой контрол ввода — там уже работают, и двух автофокусов на странице не бывает. Кнопка или ссылка, которой пришли на страницу, автофокусу не помеха.

Контрол, собранный вне документа (страница рендерится во фрагмент и попадает на экран уже собранной), ждёт появления в документе и перепроверяет условия заново — за время ожидания пользователь мог и прокрутить страницу, и уйти в другое поле.

Прокрутку пользователя различает hasUserScrolled() из @brandup/ui-kit: считаются жесты (колесо, свайп, полоса прокрутки, автопрокрутка средней кнопкой, клавиши прокрутки, действие которых никто не отменил), а не событие scroll — его поднимает и программная прокрутка. UiKitMiddleware забывает прокрутку при переходе на другую страницу — перед тем, как её нарисуют.

Интерфейс IInputControl

import type { IInputControl } from "@brandup/ui-input";

interface IInputControl {
    get form(): HTMLFormElement | null;
    get disabled(): boolean;
    get required(): boolean;
    get readonly(): boolean;
    get autoFocus(): boolean;

    validate(): boolean;
    focus(scroll?: ScrollLogicalPosition): void;
    destroy(): void;
}

Защищённые члены для наследников

| Член | Описание | | --- | --- | | __valueElem | Поле-носитель значения | | __syncValue(): void | Хук: довести значение до поля-носителя, если контрол держит его отдельно. Зовётся перед каждым чтением значения снаружи | | __focusValue(): void | Хук: куда именно ведёт фокус контрола. По умолчанию — поле-носитель; общие проверки и прокрутку делает focus() | | __requestSubmit(): void | Неявная отправка формы (Enter), как у обычного input: через form.requestSubmit() с первой кнопкой отправки | | __submitForm(): void | Досылает форме синтетический submit — движкам без requestSubmit() | | __applyAutoFocus(): boolean | Ставит автофокус, если поле его объявило. Зовётся наследником в конце конструктора; возвращает, поставлен ли фокус сейчас (отложенный до появления в документе даёт false) | | static isAutoFocus(valueElem: HTMLElement): boolean | Объявлен ли автофокус: autofocus либо data-autofocus. Статический — нужен и до super(...) | | static isReadonly(valueElem: HTMLElement): boolean | Признано ли поле только для чтения: атрибут readonly либо data-readonly (последний нужен полям без нативного атрибута, например select). Статический, потому что режим нужен и до super(...) — он влияет на сборку разметки контрола | | static prepareValueElem(valueElem, container, inputClass) | Переносит собственные классы поля на контейнер, а класс-скрыватель — на поле |

ValueElemOverrides описывает, что контрол навязал полю и что вернуть при destroy:

export interface ValueElemOverrides {
    /** Класс, добавленный полю контролом (обычно уводит его с экрана). */
    class?: string;
    /** Подменённые атрибуты: имя и исходное значение (`null` — атрибута не было). */
    attrs?: [name: string, value: string | null][];
}

Создание собственного компонента

import { InputControl } from "@brandup/ui-input";

class MyInput extends InputControl<HTMLInputElement> {
    constructor(inputElem: HTMLInputElement) {
        const wrapper = document.createElement("div");
        inputElem.insertAdjacentElement("afterend", wrapper);
        wrapper.appendChild(inputElem);

        super("MyInput", wrapper, inputElem);
    }

    getValue(): string {
        return this.__valueElem.value;
    }
}

EditorInputControl

EditorInputControl<TEditor, TChangeData, TEvents> — база контролов, где ввод идёт не в само поле, а в редактор рядом: поле-носитель уводится с экрана, но остаётся в форме (отправка, валидация, FormData). На нём построены TextBox и MessageEditor.

Класс берёт на себя общую механику: синхронизацию отложенного изменения редактора с полем, зеркало фокуса классом focused на корневом элементе, гашение нативного change скрытого поля, выравнивание редактора после form.reset() и снятие всего этого при destroy(). Доменное — фильтры ввода, подсветка, кнопки — остаётся в наследниках.

Публичные методы

| Метод | Описание | | --- | --- | | getValue(): string | Значение поля-носителя; сначала доводит отложенное изменение редактора | | setValue(value: string): void | Передаёт значение редактору — тот нормализует его и поднимет своё изменение | | hasValue(): boolean | Есть ли непустое значение | | onChange(handler): void | Подписка на событие изменения контрола (имя события задаёт EditorControlInit.changeEvent) | | caret | Куда контрол ставит каретку при фокусе из кода (EditorControlInit.caret, по умолчанию end) |

Защищённые члены

| Член | Описание | | --- | --- | | __editor | Редактор контрола. Появляется только после __attachEditor | | __listenerAbort | AbortController, одним сигналом снимающий слушатели контрола и таймеры наследников | | __attachEditor(editor): void | Передать базовому классу созданный редактор — с этого момента им владеет база | | __refreshValidity(): void | Хук: освежить собственное ограничение контрола (setCustomValidity) на поле-носителе. Зовётся при каждой синхронизации значения | | static wrapValueElem(valueElem, container, inputClass, editable, disabled) | Скрыть поле-носитель, подменить tabindex (в фокус попадает редактируемый элемент) и обернуть поле контейнером |

ValueEditor

Структурный контракт редактора — ровно то, что зовёт база. Не тип из @brandup/ui-richeditor: этот пакет — общая база всех контролов ввода, и потребители без редактора (например, dropdown) не должны тянуть его за собой. RichEditor подходит под контракт как есть, но подойдёт и любая другая реализация.

export interface ValueEditor {
    /** Редактируемый элемент — он принимает фокус вместо уведённого с экрана поля-носителя. */
    readonly editable: HTMLElement;
    /** Заменяет содержимое редактора; редактор нормализует значение и поднимает своё изменение. */
    setValue(value: string): void;
    /** Доставляет отложенное изменение немедленно — перед чтением значения извне. */
    flushChange(): void;
    /** Фокус в редактор; `atEnd` — ставить ли каретку в конец текста, если её ещё не было. */
    focus(atEnd?: boolean): void;
    destroy(): void;
}

EditorControlInit

Что базовому классу нужно знать о конкретном контроле — передаётся последним аргументом super(...).

export interface EditorControlInit {
    /** Имя события изменения контрола — на него подписывает onChange. */
    changeEvent: string;
    /** Куда ставить каретку при фокусе из кода. По умолчанию — в конец текста. */
    caret?: FocusCaret;
    /** @deprecated Замещён caret и учитывается, только если тот не задан: true — "end", false — "start". */
    focusAtEnd?: boolean;
}

FocusCaret

Куда встаёт каретка, когда фокус в контрол ставят из кода — автофокусом или вызовом focus(). Фокус мышью и с клавиатуры это не трогает: там место каретки выбирает пользователь.

| Режим | Поведение | | --- | --- | | end | В конец текста — по умолчанию: фокус получают, чтобы продолжать писать, а не чтобы вставлять перед написанным | | start | В начало текста | | all | Выделить весь текст, чтобы следующий ввод его заменил |

Режим по умолчанию доверен редактору: каретку, уже стоявшую в содержимом, он бережёт сам — правку продолжают там, где её прервали. start и all этому не подчиняются: их просили явно, поэтому контрол ставит их сам и после редактора.

Режим из разметки разбирает parseFocusCaret(value) — всё, кроме объявленных режимов, даёт end:

import { parseFocusCaret } from "@brandup/ui-input";

const caret = parseFocusCaret(valueElem.dataset.caret);

Порядок конструирования

Редактор создаёт наследник: его опции замыкаются на this и собираются только после super(...). Сразу после создания редактор обязан уйти в __attachEditor — до этого момента базовый класс уже привязал элемент, повесил слушатели формы и включил авторазрушение по удалению из DOM, но редактора у него ещё нет. Методы базы этот промежуток терпят (падение конструктора наследника не оставляет на странице обработчиков, которые ломали бы отправку любой формы), но контрол без редактора не работает.

import { EditorInputControl, type ValueEditor } from "@brandup/ui-input";

const CHANGE_EVENT = "myeditor-change";

class MyEditorControl extends EditorInputControl<MyEditor, ChangeData, MyEditorEvents> {
    constructor(valueElem: HTMLInputElement) {
        const editable = document.createElement("div");
        const container = document.createElement("div");
        container.appendChild(editable);

        // скрыть поле, подменить tabindex и обернуть контейнером
        MyEditorControl.wrapValueElem(valueElem, container, "myeditor-input", editable, valueElem.disabled);

        super("My.EditorControl", container, valueElem, { class: "myeditor-input", attrs: [] }, {
            changeEvent: CHANGE_EVENT,
        });

        this.__attachEditor(new MyEditor(editable, { value: valueElem.value }));
    }
}

LESS

source/input.less содержит рецепт поля-носителя, уведённого с экрана: поле остаётся в форме (отправка, валидация, FormData), но не показывается — вводом управляет UI контрола. Общего класса у поля нет: каждый контрол вешает свой (ui-textbox-input, ui-messageeditor-input, ui-dropdown-input), поэтому рецепт оформлен миксином.

@import (reference) "@brandup/ui-input/source/input.less";

.my-control-input {
    .ui-input-hidden-value();
}

Так его подключают @brandup/ui-textbox, @brandup/ui-messageeditor и @brandup/ui-dropdown — см. предупреждение о версиях в начале файла.