@brandup/ui-messageeditor
v1.2.12
Published
Message editor styled as a chat bubble.
Maintainers
Readme
@brandup/ui-messageeditor
Поле ввода сообщения, оформленное как плашка в чате мессенджера: скруглённый пузырь, растущий по содержимому.
Устроен по тому же принципу, что и @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.
