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-messageeditor

v1.2.12

Published

Message editor styled as a chat bubble.

Readme

@brandup/ui-messageeditor

Build Status

Поле ввода сообщения, оформленное как плашка в чате мессенджера: скруглённый пузырь, растущий по содержимому.

Устроен по тому же принципу, что и @brandup/ui-textbox: исходный input/textarea остаётся носителем значения и участвует в форме (валидация, submit, FormData), а ввод ведёт @brandup/ui-richeditor в соседнем редактируемом элементе.

Установка

npm i @brandup/ui-messageeditor

Использование

import MessageEditor from "@brandup/ui-messageeditor";

const elem = document.querySelector("textarea") as HTMLTextAreaElement;
const editor = new MessageEditor(elem);

editor.onChange(({ value }) => console.log(value));

Конструктор вставляет плашку на место поля и переносит само поле внутрь — оно скрыто, но остаётся в форме и хранит значение.

Поле может объявить автофокус — нативным autofocus либо data-autofocus: плашка получает фокус сразу после сборки, а каретка встаёт в конец текста. Когда автофокус отменяется — см. @brandup/ui-input.

Свойства

| Свойство | Тип | Описание | | --- | --- | --- | | placeholder | string \| null | Текст-заглушка; берётся из атрибута placeholder поля | | personalization | boolean | Включена ли персонализация; по умолчанию нет | | newVariables | boolean | Разрешены ли свойства не из списка: необъявленный ключ — не ошибка, а заявка; из опций либо из атрибута data-new-variables (по умолчанию выключен — необъявленный ключ ошибка) | | allowUnknownVariables | boolean | Снимает запрет отправки при необъявленном ключе, ничего больше не меняя; из опций либо из атрибута data-allow-unknown-variables (по умолчанию выключено) | | variables | MessageVariable[] | Свойства персонализации; из опций либо из атрибута data-variables | | variablesEmpty | string \| null | Текст в окне персонализации при пустом списке; из опций либо из атрибута data-variables-empty | | variablesSetup | string \| (() => void \| boolean) | Настройка полей: адрес или действие для ссылки в окне персонализации; адрес — и из атрибута data-variables-setup, со схемой, годной для href | | variablesSetupText | string | Подпись ссылки на настройку; из опций либо из атрибута data-variables-setup-text (по умолчанию MESSAGEEDITOR.TEXT.VARIABLES_SETUP) | | blocks | BlockType[] | Типы блоков сообщения: quote, code; из опций либо из атрибута data-blocks (по умолчанию все) | | tools | FormatTool[] | Инструменты форматирования; из опций либо из атрибута data-tools (по умолчанию все) | | keepFocus | boolean | Держать ли фокус, пока открыт попап смайликов (по умолчанию да, а на сенсорном устройстве нет) | | source | boolean | Показ выхода: переключатель над плашкой и панель с разметкой значения; из опций либо из атрибута data-source (по умолчанию выключен) | | variableLength | number | Сколько символов отводится свойству при подсчёте messageLength; из опций либо из атрибута data-variable-length (по умолчанию 30) |

Булевы признаки читаются вместе со значением, а не по одному присутствию атрибута: data-personalization="false" и data-personalization="0" значат «выключено», пустое значение (data-personalization), true и 1 — «включено», нет атрибута — тоже «выключено». Атрибут ставит сервер, а шаблон обычно печатает в него значение, а не решает, писать ли его вовсе.

Многострочность включена всегда: сообщение — это абзацы. Enter даёт один перенос (\n), два нажатия — пустую строку (\n\n), как в мессенджерах; форму он не отправляет.

Форматирование тоже включено всегда: панель с объявленными инструментами (по умолчанию — все) и кнопкой очистки формата всплывает над плашкой при фокусе. Кнопка очистки гаснет, когда снимать нечего. Вставка смайлика вынесена из панели в собственную кнопку внутри плашки — она доступна сразу, не дожидаясь фокуса в поле. Первой группой в попапе стоят недавно выбранные символы; помнит их @brandup/ui-richeditor — в localStorage, общем для всех редакторов источника. В disabled не строится ни панель, ни кнопка.

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

Панель и попап смайликов вместе не показываются: пока открыт попап, панель убрана — это два всплывающих слоя над одним полем. Закрылся попап — панель возвращается, если поле осталось в фокусе.

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

Состояния disabled, readonly и required берутся у поля-носителя, как у остальных контролов кита. Разметку значения плашка показывает и в них — переключать её там нечем, но показать читателю жирный жирным она обязана; звёздочки вместо него означали бы не то, что дойдёт до получателя.

Набор разметки

«Мессенджер» — это не один канал, и понимают они разное: подчёркивания и спойлера нет в WhatsApp, цитаты нет в VK, разметки нет в SMS вовсе. Кнопка, разметку которой канал не покажет, хуже отсутствующей — размеченный ею текст уйдёт получателю либо голым, либо сырыми маркерами. Поэтому и инструменты, и типы блоков объявляются набором: опцией или атрибутом поля-носителя (значения через пробел, пустое значение — пустой набор). По умолчанию доступно всё.

<textarea data-tools="bold italic strike" data-blocks="quote"></textarea>
<textarea data-tools="" data-blocks=""></textarea>

| | Опция | Атрибут | Значения | Пустой набор | | --- | --- | --- | --- | --- | | Инструменты | tools | data-tools | bold, italic, strike, underline, spoiler, code | Текст без разметки | | Типы блоков | blocks | data-blocks | quote, code | Только обычный текст |

Порядок значений в атрибуте не важен: кнопки идут в каноническом порядке набора, незнакомые значения отбрасываются. Переданный в опциях набор имеет приоритет над атрибутом — приложение знает канал точнее, чем разметка от сервера.

Отдельного набора для разбора значения нет: ограничение действует и на него. Разметка снятого инструмента остаётся в тексте как есть — на экране сырыми маркерами, в значении ровно такой, какой была, — и уезжает обратно без потерь. Так же её увидит и получатель в канале, который её не знает. Кнопка очистки формата исчезает вместе с последним инструментом: снимать становится нечего. Отмена, повтор и кнопки рандомизации со свойствами от набора не зависят — это не разметка, и канала они не касаются.

Тем же набором разбирается и вставка. Значение хранится маркдауном, поэтому маркеры во вставляемом простом тексте значат то же, что в значении, и разбираются так же: **раз** из буфера становится жирным, а маркер снятого инструмента остаётся текстом. Это касается и текстовых файлов (.md, .txt), брошенных в плашку, — файл вставляется содержимым через тот же разбор (см. @brandup/ui-richeditor).

Моноширинный и блок кода делит одна кнопка — как в мессенджерах: выделена одна строка или её часть, значит моноширинный, а несколько строк (или ничего не выделено) — блок из этих строк, остальной текст остаётся как был. Внутри кода остальные инструменты выключены: значение берёт оттуда голый текст. Внутри цитаты и блока кода Enter заканчивает блок и возвращает к обычному тексту, а строку внутри блока переносит Shift+Enter. Выйти из блока можно и Backspace в его начале, и той же кнопкой панели. Подробности разбора и хранения — в @brandup/ui-richeditor.

Показ выхода

Значение хранится не HTML, а разметкой мессенджеров (markdown), и иногда её нужно видеть целиком — сверить перед отправкой, скопировать в шаблон, показать поддержке, что именно уйдёт в канал. Для этого над плашкой встаёт переключатель на два режима, а рядом с ней — панель, в которую рендерится выход: Текст показывает сообщение, Markdown — его разметку.

По умолчанию ничего этого нет: пишущему сообщение сырая разметка не нужна, а показанная без спроса требует объяснений. Включается опцией source или атрибутом data-source поля-носителя:

<textarea data-source></textarea>
new MessageEditor(elem, { source: true });

Название второй кнопки — это формат хранения (Markdown), в него и переключают. Панель показывает ровно то, что лежит в поле-носителе и уедет в форму, поэтому отложенное при печати изменение доставляется перед показом. Значение от переключения не меняется: панель только показывает, править по-прежнему можно в плашке — текст в ней выделяется и копируется, но не редактируется. Пока панель открыта, показанное следует за значением: его меняет и хост через setValue, и окно правки, открытое до переключения.

Два вида одного значения разом на экране не держатся: панель показывается вместо плашки, иначе непонятно, какой из них правится. Фокус при переключении уходит из плашки — панель форматирования иначе висела бы над скрытым текстом. Обратно в неё он встаёт вместе с прежней кареткой, когда режим переключают кнопкой: нажали в поле — значит, продолжают писать. В disabled и readonly переключатель работает (смотреть значение — не значит изменять его), но фокус там не возвращается: в readonly фокус выделяет всё сообщение целиком, и смена вида делала бы то же самое.

Переключение из кода фокус не трогает: setValue и toggleSource зовут когда угодно, а уводить фокус со страницы в поле — и поднимать на сенсорном устройстве экранную клавиатуру — оно не должно. Нужно поведение кнопки — просят его вторым аргументом.

editor.toggleSource(true); // показать разметку
editor.toggleSource(); // обратно к сообщению, фокус остаётся где был
editor.toggleSource(false, true); // обратно к сообщению вместе с фокусом и кареткой

Свойства и рандомизация

Две доменные кнопки в панели, которых нет у обычного редактора. Обе открывают модальное окно и вставляют результат в каретку; выделение при этом не теряется — панель не забирает фокус.

Персонализация {ИМЯ} — выбор свойства из списка. По умолчанию выключена: без неё нет ни кнопки, ни подсветки, и {ИМЯ} остаётся обычным текстом. Включается опцией personalization, атрибутом data-personalization или самим объявлением списка — объявили, значит нужна. В значение уходит ключ ({ИМЯ}), а в тексте и в списке показывается название, если оно задано. Подставляет свойство приложение при отправке, компонент только вставляет разметку.

Рандомизация [раз|два] (спинтакс) — варианты набираются вручную, вариант выбирается при отправке. Выделенный текст попадает в окно первым вариантом — уже без символов самой конструкции ([, ], |) и в одну строку: внутри варианта они развалили бы спинтакс, поэтому их не вводят и не вставляют, и текст со стороны проходит ту же чистку. Без выделения первым вариантом становится слово под кареткой — рандомизируют чаще всего то слово, на котором стоят, — и собранный спинтакс встаёт на его место, а не разрывает слово; каретка вне слова (за точкой) открывает окно пустым. Готовый спинтакс, открытый на правку кликом, разбирается на варианты как есть.

Обе конструкции подсвечиваются прямо в тексте и атомарны: в поле их не отредактировать, клик по конструкции открывает то же окно для правки. Backspace и Delete у конструкции стирают её целиком — стереть в ней символ нельзя, а нативное удаление рядом с нередактируемым элементом браузеры делают по-разному; за конструкцией в конце строки к тому же стоит невидимая опора каретки, и нажатие уходило бы на неё. Удаление словами и строками (Ctrl, Alt, Cmd) остаётся браузеру: это правка текста вокруг, а не самой конструкции. При наведении подсветка темнеет, а курсор — стрелка: конструкция кликабельна, и это видно до клика. В readonly и disabled клик не работает — там нет ни отклика, ни курсора. Форматирование обходится с ними как с одним символом — выделили текст со свойством и сделали жирным, получится **раз {ИМЯ} два**, а не три отдельных куска. Разметки внутри конструкции не бывает вовсе: дописали скобки вокруг уже жирного слова — поле склеивает конструкцию и снимает разметку внутри неё, иначе в значение ушло бы {**ИМЯ**}, чего подстановка не разберёт. Подсветка — это обычные span, которые сериализация отбрасывает, поэтому в значение она не попадает: там остаётся ровно {ИМЯ} и [раз|два].

Список свойств задаётся опцией (она же включает персонализацию):

new MessageEditor(elem, {
    variables: [{ key: "ИМЯ", name: "Имя подписчика" }, { key: "ГОРОД" }],
});

либо атрибутом data-variables поля-носителя — когда разметку отдаёт сервер и передавать список неоткуда:

<textarea data-variables="ИМЯ, ГОРОД"></textarea>

<textarea data-variables='[{"key":"ИМЯ","name":"Имя подписчика"},"ГОРОД"]'></textarea>

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

Каким может быть ключ. По краям — буква, цифра или подчёркивание ({ИМЯ}, {_id}, {USER_NAME_}), внутри — что угодно в пределах строки, кроме символов разметки {}[]|. Пробелы, точки и дефисы в середине ключа допустимы ({ИМЯ КЛИЕНТА}, {order.total}), по краям — нет: там они неотличимы от обычного текста вокруг скобок, и { } или {...} становились бы свойством на ровном месте. Написанное с негодными границами конструкцией не считается вовсе — это обычный текст, каким его и набрали. Ключ, объявленный с негодными границами, отбрасывается с сообщением в консоль (пробелы по краям при этом просто срезаются).

Свойство, набранное или вставленное названием, поле само приводит к ключу: {Имя подписчика} становится {ИМЯ}. И в поле, и в списке оно показано названием — набирают его с экрана, тем, что видно, — а подставить название нечем: в сообщение уходит ключ, и получателю такое свойство ушло бы скобками наружу. Подмена происходит сразу, пока пишущий видит результат, и на экране от неё ничего не меняется: там снова название. Лишние пробелы в написанном названии не важны — название человеческий текст, а не код, — а объявленный ключ важнее чужого названия: написанное, совпавшее с ключом, остаётся как есть.

Ключи регистронезависимы. Их тоже набирают руками, глядя на экран, поэтому {имя}, {Имя} и {ИМЯ} — одно и то же свойство. Сверка идёт без оглядки на регистр, а в сообщение уходит объявленный ключ: написанное приводится к нему той же подменой, что и название, — подстановка на стороне приложения ищет ключ буква в букву. Два объявленных ключа, различающихся только регистром, — дело приложения: в тексте они неразличимы, и берётся первый объявленный.

Строгий режим: чужое свойство

По умолчанию список свойств закрытый: свойство с ключом, которого в нём нет, помечается в тексте отдельно — заливкой и волнистым подчёркиванием (span.variable.unknown), с подсказкой в title. Подставить его нечем: на отправке оно уйдёт получателю скобками наружу, а выглядит при этом ровно как рабочее, и опечатка в ключе замечается уже по отправленному сообщению.

Пустой список — не исключение: не объявлено ничего, значит чужое любое свойство в тексте. Приложению, которому набор ещё не известен (свойства появляются после выбора аудитории), персонализацию до тех пор включать незачем: без неё {ИМЯ} — обычный текст, а включают её тогда же, когда узнают набор.

Это ошибка значения, а не оформления, поэтому она же останавливает отправку формы: контрол объявляет её полю-носителю через setCustomValidity, дальше решает браузер — как и с нативным required. Проверяется и начальное значение, при сборке: разметку отдаёт сервер, а нативная проверка ограничений идёт до события submit — без неё первая отправка ушла бы с чужим свойством. Пока включена персонализация, подпись невалидности на поле принадлежит контролу: свою приложению придётся держать где-то ещё. По destroy она снимается — иначе поле осталось бы в форме навсегда невалидным, а объяснить это было бы нечем. Список ключей отдаёт свойство unknownVariables:

editor.onChange(() => {
    const unknown = editor.unknownVariables; // ["ИМЯЯ"]
    hint.textContent = unknown.length ? `Нет таких свойств: ${unknown.join(", ")}` : "";
});

Режим новых свойств

Список бывает и открытым — когда свойства заводит внешняя система по мере того, как их набирают в сообщении. Включается опцией newVariables или атрибутом data-new-variables (он же — согласие на персонализацию, как и объявленный список):

new MessageEditor(elem, {
    variables: [{ key: "ИМЯ", name: "Имя подписчика" }],
    newVariables: true,
});
<textarea data-variables="ИМЯ, ГОРОД" data-new-variables></textarea>

Необъявленный ключ здесь не ошибка, а заявка на свойство, которое заведёт приложение. Отличия от строгого режима:

  • отправка не останавливается — setCustomValidity контрол не трогает вовсе, и подпись невалидности на поле остаётся приложению;
  • в тексте такое свойство помечается своим цветом и пунктирным подчёркиванием (span.variable.new, а не .unknown) — не как ошибка, а как ещё не заведённое поле;
  • в окне персонализации оно становится записью с пометкой «новое» — следом за объявленными, вставляется так же, как они. Список собирается на каждое открытие, поэтому набранное только что свойство находится в нём сразу;
  • клик по ней открывает правку ключа, а не список (заголовок у обоих окон один (MESSAGEEDITOR.TEXT.VARIABLES_TITLE): для набирающего это одно и то же действие, в текст в обоих случаях встаёт {КЛЮЧ}): выбирать новое свойство не из чего — его набрали здесь же, и опечатку в нём исправляют текстом. Окно принимает то же, что и объявление списка (isVariableKey): символы разметки в поле не набираются и не вставляются, краевые пробелы срезаются при сохранении, а пока набранное нельзя объявить — кнопка сохранения погашена. Enter сохраняет. Объявленное свойство по-прежнему меняют выбором из списка, и в строгом режиме необъявленное — тоже: там оно ошибка, и исправить её можно только объявленным;
  • пустой объявленный список работает — в строгом режиме он делает чужим любое свойство, а здесь это рабочее начало: заводить свойства и предстоит с нуля.

Неполный список без режима заявок

Список бывает неполон и не по ошибке: свойства заводятся на стороне, набор приезжает частями, часть ключей приложение подставляет само. Держать отправку в таком случае нечем, а всё остальное про чужой ключ пригождается — для этого есть allowUnknownVariables (атрибут data-allow-unknown-variables):

new MessageEditor(elem, {
    variables: [{ key: "ИМЯ", name: "Имя подписчика" }],
    allowUnknownVariables: true,
});

Снимается ровно одно — запрет отправки. Ключ по-прежнему подсвечивается как чужой (span.variable.unknown) и по-прежнему перечислен в unknownVariables, так что предупреждение рядом с полем приложение показывает само, когда сочтёт нужным. Подпись невалидности на поле-носителе при этом принадлежит приложению: редактор её не ставит и не стирает.

Не путать с newVariables: тот объявляет необъявленный ключ заявкой и меняет поведение — свой цвет пометки, запись в окне персонализации, окно правки ключа. Здесь ничего этого нет. И сам по себе флаг персонализацию не включает — это послабление проверки, а не заявление о том, что свойства используются.

Новым считается только то свойство, которое можно завести ровно таким, как написано. Ключ с символами разметки ({A|B}) объявить нельзя — такой помечается чужим, как в строгом режиме, и в unknownVariables не попадает: заявка имеет смысл, только если её можно выполнить. Написанное с негодными границами ({ }, {...}) конструкцией не становится вовсе.

Ключ нового свойства приводится к верхнему регистру: {скидка} в тексте становится {СКИДКА} — той же подменой, что приводит названия к ключам, и так же сразу, пока пишущий видит результат. Объявить такой ключ пока нечем, а заведут ровно то, что набрано, — и {Скидка} рядом с {СКИДКА} развели бы у внешней системы два поля вместо одного. В строгом режиме регистр набранного не правится: необъявленный ключ там ошибка, и исправлять её пишущему.

Что предстоит завести, отдаёт то же свойство unknownVariables — отправку оно не держит, но выполнить его кому-то придётся:

form.addEventListener("submit", async () => {
    for (const key of editor.unknownVariables) await api.createVariable(key);
});

Пустой список — не ошибка: кнопка остаётся на месте, а окно объясняет, почему выбирать нечего. Текст задаётся опцией variablesEmpty или атрибутом data-variables-empty, по умолчанию — подпись MESSAGEEDITOR.TEXT.VARIABLES_EMPTY из реестра (см. «Локализация»). Причину знает приложение: свойства могут появиться после выбора аудитории, а могут быть не предусмотрены вовсе. Проверку пустой список не отменяет: в строгом режиме чужим становится любое свойство в тексте, в режиме новых — новым. Поля контрол не касается вовсе, только пока персонализация выключена: выставленную приложением подпись setCustomValidity в этом случае не стирает.

<textarea data-variables-empty="Свойства появятся после выбора аудитории."></textarea>

Настройка полей

Список свойств где-то ведётся, и из окна персонализации туда должно быть видно дорогу — особенно когда список пуст или нужного поля в нём не нашлось. Ссылка на настройку встаёт последней строкой окна, в обоих состояниях списка одинаково: при пустом это главный выход, при заполненном — запасной. Без объявленной настройки строки нет вовсе.

Где настройка живёт, знает приложение: строка — адрес перехода, и рисуется настоящая <a href> (работают средняя кнопка и «копировать адрес»); функция — действие хоста, SPA-переход или собственное окно, и рисуется кнопка в виде ссылки. Схема адреса проверяется той же проверкой, что и у ссылок редактора (safeUrl из @brandup/ui-richeditor): javascript: и data: в href — исполнение кода, пришедшего вместе с разметкой, а атрибут обычно печатает сервер. Негодный адрес отбрасывается с сообщением в консоль — настройки тогда как будто не объявляли вовсе, и строки в окне нет. Объявленная настройка — тоже согласие на персонализацию, как и объявленный список свойств.

new MessageEditor(elem, { variablesSetup: "/settings/fields" });
new MessageEditor(elem, { variablesSetup: () => app.nav("/settings/fields") });
<textarea data-variables-setup="/settings/fields" data-variables-setup-text="Управление полями"></textarea>

По нажатию окно закрывается молча — без возврата каретки в поле: фокус уходит на другой экран, и мигающая каретка в оставленном поле только сбивала бы с толку. Функция может попросить окно не трогать, вернув false, — например, когда открывает собственный слой поверх; действие при этом выполняется до закрытия. Подпись задаётся опцией variablesSetupText или атрибутом, по умолчанию — подпись MESSAGEEDITOR.TEXT.VARIABLES_SETUP из реестра (см. «Локализация»).

Длина сообщения

У каналов есть лимиты на длину сообщения, а у сообщения с конструкциями нет одной длины: спинтакс уйдёт одним из вариантов, свойство развернётся в подставленное значение. Свойство messageLength отдаёт оценку — по видимому тексту, без маркеров форматирования, обрезанную по краям, как значение из getValue():

  • спинтакс [раз|два] считается самым длинным вариантом: в отправку уйдёт любой из них, и лимит обязан выдержать каждый;
  • свойство {ИМЯ} считается условной длиной подставляемого значения — по умолчанию 30 символов. Сколько на самом деле, знает приложение: задаётся опцией variableLength или атрибутом data-variable-length. Без персонализации {ИМЯ} — обычный текст и считается по буквам.

Вложенных конструкций нет: свойство внутри варианта спинтакса — часть его текста и считается по буквам, как и в проверке объявленных свойств.

<textarea data-variable-length="20"></textarea>

Своего вывода у длины пока нет: свой лимит приложение проверяет само, читая messageLength на onChange. Проверку формы длина не держит — оценка с конструкциями останавливала бы отправку по догадке. Считает ту же длину и отдельная функция messageLength(root, options), если содержимое пришло не из компонента.

API

| Метод | Описание | | --- | --- | | getValue(): string | Текущее значение (обрезанное по краям) | | setValue(value): void | Заменить значение | | hasValue(): boolean | Есть ли непустое значение | | validate(): boolean | Проверка по атрибутам поля-носителя, проставляет класс invalid | | unknownVariables | Ключи свойств из текста, которых нет в объявленном списке (в порядке появления); в режиме новых свойств — только те из них, которые можно завести: список того, что предстоит создать | | messageLength | Оценка длины сообщения: спинтакс — самым длинным вариантом, свойство — условной длиной (variableLength); считается на месте, по текущему содержимому | | sourceMode | Показан ли выход вместо плашки | | toggleSource(show?, focus?) | Переключить режим показа; без аргумента — на противоположный. Фокус по умолчанию не трогается, focus: true возвращает его в плашку вместе с кареткой (кроме disabled и readonly). Без включённого показа выхода не делает ничего | | onChange(handler) | Подписка на событие ui:messageeditor:change | | editor | Доступ к встроенному RichEditor — выделение, вставка текста | | destroy(): void | Возвращает поле на место и освобождает ресурсы; открытое окно правки закрывается, своя подпись невалидности снимается |

Валидация штатная: ограничения объявлены на поле-носителе, решение принимает браузер — он же блокирует отправку формы. Контрол только отражает результат классом invalid. Своё ограничение у него одно — неизвестное свойство, и объявляется оно тем же способом, что и у остальных контролов кита, через setCustomValidity.

Пакет экспортирует и внутренние части, если они нужны отдельно: VariablesModal, VariableKeyModal, RandomizerModal, parseVariables, buildVariable, buildSpintax, parseSpintax, messageLength, parseVariable, isVariableKey, plainVariableKey, типы VariablesSetup и LengthOptions. Классы, команды, подписи и числа — в объекте MESSAGEEDITOR (модуль names.ts): MESSAGEEDITOR.TEXT.VARIABLES_TITLE, MESSAGEEDITOR.VALUE.MAX_VARIANTS, MESSAGEEDITOR.SYNTAX.SPINTAX_OPEN и так далее.

Локализация

Подписи по умолчанию английские. Русские возит сам пакет, приложение объявляет их один раз при старте:

import { setTexts } from "@brandup/ui-kit/i18n";
import ru from "@brandup/ui-messageeditor/locale/ru.json";

setTexts(ru);

Подпись отдельного контрола задаётся на нём самом и сильнее текстов приложения. Для плашки это опции variablesEmpty и variablesSetupText и их data-*. Подробнее — @brandup/ui-kit.

Оформление

Разметка: корневой .ui-messageeditor → .bubble (плашка) → редактируемый элемент .ui-richeditor и коробка кнопки .emoji-holder с кнопкой .emoji-button справа от него. Подсвеченные конструкции в тексте — span.variable и span.spintax; необъявленное свойство — span.variable.unknown, а в режиме новых свойств — span.variable.new (ключ, который нельзя завести, остаётся .unknown и там).

Внутри конструкции символы самой разметки — скобки и разделитель вариантов — лежат в собственных обёртках span.mark (по умолчанию у каждой отступ в пиксель с обеих сторон), а между ними содержимое: варианты спинтакса обычным текстом, ключ свойства — тоже, либо в span.key, когда у него есть название. Название показывается оформлением пустой обёртки span.label (content: attr(data-label)): узел с текстом попал бы в текст поля, а из него собирается значение сообщения. Текст конструкции от этого разбиения не меняется — сериализация вложенные обёртки отбрасывает, оставляя их текст.

При включённом показе выхода перед плашкой встаёт переключатель .modes с кнопками .mode (выбранная — .active), а после неё — панель .source. Видимость плашки и панели переключает класс source-mode на корневом элементе, и вешает его сам компонент: сам режим он держит в себе, а из класса не читает — собственные классы поля-носителя переезжают на корневой элемент контрола, и class="source-mode" в разметке поля включал бы режим, которого нет. По той же причине плашку прячет только класс режима при собранной панели (.source-mode:has(> .source)).

Прокручивается текст, а не плашка: кнопка остаётся на месте, когда сообщение перерастает --messageeditor-maxheight.

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

Заливка по умолчанию своя, а не от полей ввода: белая плашка на белой странице держалась бы только на тени и почти не читалась бы.

Настраивается CSS-переменными:

| Переменная | По умолчанию | Что задаёт | | --- | --- | --- | | --messageeditor-fill | #e7f3ff | Заливка плашки | | --messageeditor-color | var(--input-color, #222) | Цвет текста | | --messageeditor-radius | 18px | Скругление плашки | | --messageeditor-minheight | var(--input-line-height, 1.2rem) | Высота содержимого пустой плашки (иначе она схлопывается в отступы) | | --messageeditor-maxheight | 220px | Высота, после которой появляется прокрутка | | --messageeditor-padding-tb | считается от --input-height | Отступы плашки сверху и снизу: такие, что пустая плашка ровно в высоту полей ввода кита, а строка стоит по центру. По горизонтали берётся --input-padding-lr, общий для полей ввода | | --messageeditor-gap | 8px | Расстояние от текста до кнопки смайлика | | --messageeditor-button-size | 32px | Размер кнопки смайлика | | --messageeditor-button-icon | 20px | Размер иконки на кнопке | | --messageeditor-button-color | var(--placeholder-color, #999) | Цвет иконки (в наведении и при открытой панели — акцент) | | --messageeditor-shadow | 0 1px 2px rgba(0,0,0,.16) | Тень плашки | | --messageeditor-border | var(--messageeditor-fill) | Рамка плашки — в цвет заливки, поэтому в покое не видна | | --messageeditor-border-width | 1px | Толщина рамки | | --messageeditor-border-focus | заливка, затемнённая на 12% | Рамка в фокусе | | --messageeditor-accent | var(--focus--input-border-color, #4a9eff) | Цвет иконки смайлика при наведении и открытой панели | | --messageeditor-spintax-fill | rgba(255,193,7,.28) | Подсветка спинтакса [раз\|два] в тексте | | --messageeditor-variable-fill | rgba(76,175,80,.24) | Подсветка свойства {ИМЯ} в тексте | | --messageeditor-variable-unknown-fill | rgba(244,67,54,.24) | Подсветка свойства, которого нет в объявленном списке | | --messageeditor-variable-unknown-mark | rgba(244,67,54,.8) | Волнистое подчёркивание под ней — отличие не только цветом | | --messageeditor-variable-new-fill | rgba(156,39,176,.2) | Подсветка нового свойства в режиме newVariables | | --messageeditor-variable-new-mark | rgba(156,39,176,.7) | Пунктирное подчёркивание под ней и цвет пометки «новая» в окне | | --hover--messageeditor-spintax-fill | заливка, затемнённая на 12% | Подсветка спинтакса при наведении | | --hover--messageeditor-variable-fill | заливка, затемнённая на 12% | Подсветка свойства при наведении | | --hover--messageeditor-variable-unknown-fill | заливка, затемнённая на 12% | Подсветка неизвестного свойства при наведении | | --hover--messageeditor-variable-new-fill | заливка, затемнённая на 12% | Подсветка нового свойства при наведении | | --messageeditor-source-fill | var(--input-fill, #fff) | Заливка панели выхода — нейтральная, как у полей ввода: это не сообщение, а его разметка | | --messageeditor-source-border | var(--input-border-color, #dcdfe4) | Рамка панели выхода | | --messageeditor-source-color | var(--messageeditor-color) | Цвет текста разметки | | --messageeditor-source-font | системный моноширинный | Шрифт разметки | | --messageeditor-modes-gap | 6px | Расстояние от переключателя до плашки | | --messageeditor-quote-fill | заливка плашки, затемнённая на 6% | Подложка цитаты в сообщении — в тон плашки, а не серая по умолчанию редактора |

Классы состояний вешаются на корневой элемент: focused, invalid, disabled, readonly, source-mode.