UI 26.675.0: ui.actionpanel переехал в бандл, а rich_text по-новому обходится с квадратными скобками
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Модуль ui 26.675.0 собран 26 августа 2026. Из PHP поменялись Converter и Whitelist в Bitrix\UI\Format\BBCode, и пользовательские поля типа rich_text теперь иначе сохраняют и индексируют текст с квадратными скобками. На фронтенде старую панель групповых действий ui.actionpanel перевели на бандл и посадили на новое расширение ui.action-panel, написанное на TypeScript. Если вы подключаете панель по прямым путям к файлам или вызываете её внутренние методы, после обновления это сломается.
Проверьте, как вы подключаете ui.actionpanel
Файлы старой панели переехали в src/, собираются в dist/actionpanel.bundle.js и dist/actionpanel.bundle.css, и config.php расширения теперь ссылается только на них. Ключи bundle_js/bundle_css со значением ui_actionpanel из конфига убрали, в rel добавились main.core и ui.action-panel.
В снапшоте после обновления по старым путям файлов нет:
/bitrix/js/ui/actionpanel/panel.js/bitrix/js/ui/actionpanel/item.js/bitrix/js/ui/actionpanel/css/style.css
Вместе с ними удалены panel.min.js, item.min.js, css/style.min.css и карты panel.map.js, item.map.js. Код, который подключает панель через Asset::addJs() или <script src> по этим путям, получит 404, и BX.UI.ActionPanel на странице не появится. Подключайте расширение по имени, тогда ядро само подтянет бандл и зависимости:
<?php declare(strict_types=1);
use Bitrix\Main\UI\Extension;
// было: Asset::getInstance()->addJs('/bitrix/js/ui/actionpanel/panel.js');
Extension::load('ui.actionpanel');
Не опирайтесь на удалённые методы BX.UI.ActionPanel
Из прототипа легаси-панели пропали пять методов: getMutationObserver(), getMutationObserverParam(), addHiddenItem(), removeHiddenItem() и handleGridUnSelectItem().
Первые два обслуживали MutationObserver, который смотрел за всем document.body с subtree: true и на любую мутацию страницы пересчитывал стиль панели. Теперь позицию ведёт PositionTracker из нового расширения. По описанию в action-panel.bundle.d.ts он следит за цепочкой родителей через ResizeObserver, слушает scroll и resize и заменяет легаси-наблюдатель за всем документом. Собственные подписки панели на scroll и resize окна тоже убраны, трекер держит их один.
Скрытые кнопки, которые не влезли в ряд, теперь считает OverflowCalculator. Метод fillHiddenItems() остался и берёт результат его recalc(). По комментарию к калькулятору, у старой панели было две расходящиеся реализации: обработчик resize считал правильно, а addItems терял блок «Ещё», если были скрытые кнопки.
Если вы наследовались от BX.UI.ActionPanel или подменяли эти методы на экземпляре, код перестанет срабатывать. Ещё поменялось поведение:
- на одном родительском контейнере теперь живёт одна панель,
draw()новой вызываетdestroy()у предыдущей. Методdestroy()у легаси-панели появился в этом же релизе; - класс
ui-action-panel-shownснимается сbody, только когда на странице не осталось показанных панелей; - обработчик клика вне панели на каждом клике заново вызывает
this.handleOuterClick(event). По комментарию в коде, потребители подменяют этот метод на экземпляре, и теперь такая подмена срабатывает.
Проверьте, что поля rich_text делают со скобками
Converter и Whitelist появились в 26.600.0 вместе с типом пользовательского поля rich_text (Bitrix\UI\UserField\Types\RichTextType). Перед сохранением поле пропускает значение через Whitelist::normalize(), а текст для поискового индекса берёт из Converter::toPlainText(). Тот же toPlainText() вызывает шаблон main.public_text компонента ui.field.richtext. В 26.675.0 оба метода стали строже к тому, что считать тегом.
Whitelist::normalize() снимает запрещённые теги и вызывает escapeDanglingBrackets(). Колбэк у последнего прежний. Всё, что совпало с паттерном тега, остаётся как есть, а одиночные [ и ] превращаются в [ и ]. Поменялся сам паттерн. Раньше тегом считался любой токен, который начинается с имени тега из белого списка как отдельного слова и тянется до закрывающей скобки, теперь только строгие формы [tag], [tag=value], [tag attr=value] и [/tag]. Поэтому [p.s.], [b 2024] и [img of the report] тегами больше не считаются, и при сохранении их скобки экранируются.
Converter::toPlainText() раньше заканчивал цепочку замен регуляркой \[/?[a-z][a-z0-9]*\b[^\]]*\], которая удаляла любой токен в скобках, начинающийся с латинской буквы ([слово] на кириллице она не трогала). Теперь цепочка начинается с запрещённых тегов. Блочные (quote, code, table, tr, td, th, spoiler, align) заменяются пробелом, чтобы соседние блоки не слипались в одно слово, остальные снимает Whitelist::stripForbiddenBbTags(). Медиа-блоки [img]…[/img] и [video]…[/video] по-прежнему вырезаются вместе с содержимым, чтобы поиск не индексировал URL встраивания, но только если открывающий тег записан строго. [list], [p] и [*] заменяются пробелом. Последними из тегов снимаются оставшиеся теги белого списка (b, i, u, s, url и непарные img/video), а остальное в скобках, по комментарию в коде, остаётся как пользовательский текст.
Я собрал обе версии классов (старую из диффа 26.600.0, в 26.650.0 она не менялась, и новую с патчем этого релиза) и прогнал через них несколько строк.
Converter::toPlainText():
| Вход | 26.650.0 | 26.675.0 |
|---|---|---|
[quote]Цена[/quote][quote]99[/quote] |
Цена99 |
Цена 99 |
Отчёт [p.s.] готов, см. [b 2024] |
Отчёт готов, см. |
Отчёт [p.s.] готов, см. [b 2024] |
См. [img of the report] ниже и картинку [img]https://bxmax.ru/a.png[/img] |
См. |
См. [img of the report] ниже и картинку |
[foo]bar[/foo] и [1] пункт |
bar и [1] пункт |
[foo]bar[/foo] и [1] пункт |
[table][tr][td]a[/td][td]b[/td][/tr][/table] |
ab |
a b |
Whitelist::normalize():
| Вход | 26.650.0 | 26.675.0 |
|---|---|---|
Отчёт [p.s.] готов, см. [b 2024] |
без изменений | Отчёт [p.s.] готов, см. [b 2024] |
См. [img of the report] ниже и картинку [img]https://bxmax.ru/a.png[/img] |
без изменений | См. [img of the report] ниже и картинку [img]https://bxmax.ru/a.png[/img] |
Строка из обычных тегов вроде [b]Жирный[/b] и [color=red]крас[/color]ный в обеих версиях даёт одно и то же: normalize() оставляет [b]Жирный[/b] и красный, toPlainText() возвращает Жирный и красный.
Если ваш код читает сырое значение поля rich_text и ищет в нём квадратные скобки, в новых записях он встретит [ и ]. Миграции для уже сохранённых значений в снапшоте не видно, так что в базе, скорее всего, будут лежать обе формы. toPlainText() после снятия тегов превращает [ и ] обратно в скобки, поэтому для сырой и для экранированной строки новая версия выдаёт одинаковый текст. В поисковый индекс поля теперь попадает больше текста: старая версия выбрасывала [p.s.] и всё от [img of the report] до ближайшего [/img].
Новое: строгие паттерны тегов в Whitelist
У Bitrix\UI\Format\BBCode\Whitelist три новых публичных статических метода:
getTagPattern(array $tags)— одиночный тег из списка в строгой форме, открывающий или закрывающий; в паттерне есть голый/, так что разделитель берите другой, например#;getPairedTagPattern(array $tags)— целый блок[tag]…[/tag], содержимое захватывается лениво;getForbiddenBlockTags()— блочная половина запрещённых тегов.
Паттерны возвращаются без разделителей. Модификаторы iu добавляете сами, для парного ещё и s. Маркер списка [*] оба метода пропускают, его вызывающий код обрабатывает отдельно.
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\UI\Format\BBCode\Whitelist;
Loader::requireModule('ui');
$tag = '#' . Whitelist::getTagPattern(['b', 'p', 'url']) . '#iu';
foreach (['[b]', '[/b]', '[p=left]', '[url=https://bxmax.ru]', '[b 2024]', '[p.s.]'] as $token) {
printf("%-24s %s\n", $token, preg_match($tag, $token) === 1 ? 'тег' : 'текст');
}
// [b], [/b], [p=left], [url=https://bxmax.ru] → тег
// [b 2024], [p.s.] → текст
$media = '#' . Whitelist::getPairedTagPattern(Whitelist::getMediaTags()) . '#isu';
echo preg_replace($media, ' ', 'см. [img of the report] ниже, картинка [img]https://bxmax.ru/a.png[/img]');
// см. [img of the report] ниже, картинка
Этот код я прогнал на Whitelist.php из этого релиза. Старый медиа-паттерн на той же строке оставлял только см..
Новое: расширение ui.action-panel
Новая панель групповых действий на TypeScript, типы лежат рядом в dist/action-panel.bundle.d.ts. Экспорты: ActionPanel, ActionPanelItem, PositionTracker, OverflowCalculator. В config.php есть настройка useAirDesign, она включается, если определена константа AIR_SITE_TEMPLATE.
Панель надо брать как BX.UI.AirActionPanel, потому что имя BX.UI.ActionPanel занято. Бандл кладёт классы в BX.UI, туда же старый ui.actionpanel кладёт свой BX.UI.ActionPanel, а старый зависит от нового, грузится позже и перезаписывает глобал (так написано в src/index.ts). На странице с легаси-расширением BX.UI.ActionPanel остаётся старым конструктором.
<?php declare(strict_types=1);
use Bitrix\Main\UI\Extension;
Extension::load('ui.action-panel');
?>
<div id="orders-grid"></div>
<script>
const panel = new BX.UI.AirActionPanel({
renderTo: () => document.getElementById('orders-grid'),
ariaLabel: 'Действия с заказами',
items: [
{ id: 'export', text: 'Выгрузить', onclick: () => exportSelected() },
{ id: 'delete', text: 'Удалить', onclick: (event, item) => deleteSelected() },
],
onResetAll: () => resetSelection(),
});
panel.setTotalSelectedItems(3);
panel.show();
</script>
renderTo принимает элемент или функцию. Контейнер может пересоздаться ajax-перерисовкой, и тогда панель около секунды повторяет попытку найти его на каждом кадре. text у кнопки вставляется текстовым узлом, разметка в нём покажется как есть. onclick принимает только функцию: строковые обработчики, которые старая панель превращала в атрибут onclick, остались в легаси-адаптере. Ещё у кнопки есть menuItems (меню ui.system.menu), closeOnItemClick, disabled, hidden, dataset, popupDataset и attributes, у панели — refreshOverflow(), getHiddenItems(), getItemById() и статический getInstanceByNode().
Мелочи и находки
- Легаси-панель пользуется новым расширением только частично. Она импортирует оттуда
PositionTrackerиOverflowCalculator, а разметку и кнопки рисует по-старому. FocusTrapизui.a11yприisolateOutsideставилinertна соседние элементы, и каждая ловушка вела свой список уже инертных, а при выключении заново искала соседей по текущему DOM. Теперь соседи запоминаются при включении, счётчик владельцев общий на все ловушки, иinertснимается, только когда элемент отпустили все, и только если он не был инертным до них.- У
BX.UI.ActionPanel.Itemв split-кнопке класс иконки приклеивался кui-btn-linkбез пробела. Починили. - Подменю кнопок легаси-панели получили свой z-index.
ActionPanel.Item.openSubMenu()и раньше передавал попапуthis.actionPanel.zIndex + 1, но панель опциюzIndexникуда не сохраняла, и подменю всегда получалоnull. Теперь конструктор кладёт её вthis.zIndex. ui.action-panelприехал только сlang/ru/config.php, других языков в списке добавленных файлов нет.- В
ui.block-diagramавтоскролл больше не стартует по нажатию, только с первым движением мыши. Раньше клик по блоку у края холста утаскивал холст. Если правило валидации порта отказало цели при протягивании связи, отказ запоминается до конца жеста и правило про эту цель больше не спрашивают. Это записано контрактом в комментарии к типуDiagramValidationPortRuleFn. Порты блоков, въехавших в видимую область во время жеста, становятся доступными целями. - Deprecated и изменений в БД нет.
Что делать
- Поищите в шаблонах и компонентах
ui/actionpanel/panel.js,ui/actionpanel/item.jsиui/actionpanel/css/style.css. Прямые подключения замените наExtension::load('ui.actionpanel'). - Если расширяли
BX.UI.ActionPanel.prototype, проверьте пять удалённых методов и то, что вторая панель на том же контейнере теперь убивает первую. - Если читаете сырые значения полей
rich_text, учтите, что в новых записях скобки вне тегов хранятся как[и]. Для вывода текста беритеConverter::toPlainText(), он понимает обе формы. - Если результат
toPlainText()у вас где-то сохранён (своя выгрузка, кэш, поисковый индекс по старым записямrich_text), пересчитайте его после обновления.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
/bitrix/js/ui/actionpanel/panel.js
|
Удалено |
Extension::load('ui.actionpanel')
|
/bitrix/js/ui/actionpanel/item.js
|
Удалено |
Extension::load('ui.actionpanel')
|
/bitrix/js/ui/actionpanel/css/style.css
|
Удалено |
Extension::load('ui.actionpanel')
|
BX.UI.ActionPanel.prototype.getMutationObserver()
|
Удалено |
BX.UI.PositionTracker
|
BX.UI.ActionPanel.prototype.getMutationObserverParam()
|
Удалено |
BX.UI.PositionTracker
|
BX.UI.ActionPanel.prototype.addHiddenItem()
|
Удалено |
BX.UI.ActionPanel.prototype.fillHiddenItems()
|
BX.UI.ActionPanel.prototype.removeHiddenItem()
|
Удалено |
BX.UI.ActionPanel.prototype.fillHiddenItems()
|
BX.UI.ActionPanel.prototype.handleGridUnSelectItem()
|
Удалено | — |