Visualeditor 26.0.0: редактор HTML-страниц, который подключают своим кодом
С обновлениями пришёл новый модуль «Визуальный редактор» (visualeditor). Он открывает HTML-страницу сайта вместе с её стилями. Выбрали элемент, поправили текст, ссылку или картинку, и правка ложится точно в исходный файл, остальной код не переформатируется.
Модуль доступен и в «Управлении сайтом», и в коробочном Битрикс24, но сам ничего не включает. Это готовый безопасный редактор без хранилища и прав. Какие страницы открывать и кому их править, решает ваш код.
Новое
Установите модуль из списка обновлений
В списке обновлений модуль помечен как «Новый». Отметьте его и установите вместе с остальными. Установщик копирует JS в /bitrix/js/visualeditor/ и регистрирует модуль, таблиц и настроек нет.
После установки в админке не появится ни страницы, ни пункта меню, ни компонента. Штатные модули коробки редактор пока не используют: ни один не регистрирует для него адаптер.
SDK редактора уже внутри
В модуль целиком входит Visual Editor SDK 1.15.0. Он показывает список страниц сайта, открывает выбранную с ресурсами сайта и даёт кликнуть по элементу. Дальше правка текста, атрибутов и изображений, отмена правок, предпросмотр на весь экран. Для HTML-презентаций есть навигация по слайдам, для ИИ — кнопка «Спросить ИИ». Интерфейс переведён на десять языков, русский среди них.
Напишите адаптер к своим страницам
Где лежат документы и кто их правит, модуль не знает. Это знает адаптер — ваш класс с интерфейсом EditorAdapterInterface. В нём три метода, list(), read() и save(), и каждый возвращает Bitrix\Main\Result.
Пример: маркетологи правят промостраницы из папки /promo/, доступ только у администраторов.
<?php
declare(strict_types=1);
namespace Local\Promo;
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Error;
use Bitrix\Main\IO\Directory;
use Bitrix\Main\IO\File;
use Bitrix\Main\Result;
use Bitrix\VisualEditor\Public\Adapter\EditorAdapterInterface;
final class PromoPagesAdapter implements EditorAdapterInterface
{
private const DIR = '/promo/';
public function list(array $context): Result
{
if (!$this->canEdit()) {
return $this->denied();
}
$paths = [];
foreach ((new Directory($this->root()))->getChildren() as $item) {
if ($item instanceof File && $item->getExtension() === 'html') {
$paths[] = $item->getName();
}
}
return (new Result())->setData(['paths' => $paths, 'truncated' => false]);
}
public function read(array $context, string $path): Result
{
$file = $this->file($path);
return $file === null
? $this->denied()
: (new Result())->setData(['content' => $file->getContents()]);
}
public function save(array $context, array $changeSet): Result
{
$file = $this->file($changeSet['path']);
if ($file === null) {
return $this->denied();
}
return $file->putContents($changeSet['content']) === false
? (new Result())->addError(new Error('Файл не записан', 'write-error'))
: new Result();
}
private function file(string $path): ?File
{
if (!$this->canEdit() || preg_match('/^[a-z0-9-]+\.html$/D', $path) !== 1) {
return null;
}
$file = new File($this->root() . $path);
return $file->isExists() ? $file : null;
}
private function canEdit(): bool
{
return CurrentUser::get()->isAdmin();
}
private function root(): string
{
return $_SERVER['DOCUMENT_ROOT'] . self::DIR;
}
private function denied(): Result
{
return (new Result())->addError(new Error('Нет доступа', 'access-denied'));
}
}
Пример проверен на «Управлении сайтом» с модулем 26.0.0: правка заголовка записалась в файл, путь с ../ отклонён, повторное сохранение со старым хешем вернуло stale-version.
Модуль проверяет ответы адаптера: list() обязан вернуть paths и truncated, read() — ключ content. В save() приходят готовые байты. Модуль сам перечитал файл через read(), применил правки и положил результат в $changeSet['content'], адаптеру остаётся записать.
$context и путь приходят из браузера как есть. Контроллер требует только авторизацию, POST и CSRF, так что любой авторизованный пользователь может вызвать visualeditor.editor.read с вашим ID адаптера. Права и белый список путей проверяйте в каждом методе, как в file() выше.
Зарегистрируйте адаптер и откройте редактор
Адаптер регистрируют в обработчике события visualeditor:onRegisterEditorAdapters. Рекомендуемый ID — <модуль>.<ресурс>. Повторная регистрация того же ID бросит исключение.
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Local\Promo\PromoPagesAdapter;
EventManager::getInstance()->addEventHandler(
'visualeditor',
'onRegisterEditorAdapters',
static function (Event $event): void {
$event->getParameter('registry')->register('local.promo', new PromoPagesAdapter());
},
);
Класс адаптера подключите автозагрузкой. На странице редактора, доступной только авторизованным, загрузите расширение и вызовите VisualEditor.mount():
<?php \Bitrix\Main\UI\Extension::load('visualeditor.editor'); ?>
<div id="promo-editor" style="height: 80vh"></div>
<script>
VisualEditor.mount(document.getElementById('promo-editor'), {
adapterId: 'local.promo',
context: {},
editorOptions: { siteUrl: location.origin + '/promo/' },
});
</script>
siteUrl обязателен: абсолютный адрес со слешем на конце. От него браузер грузит стили и картинки страницы. Без слеша относительные адреса будут считаться от папки уровнем выше.
Картинки по умолчанию попадают в HTML
Загруженная картинка встраивается прямо в разметку как data:-URL. Для пары иконок это терпимо, а фотографии раздуют страницу. Чтобы сохранять файлы, передайте в mount() параметр features: { uploadMode: 'file' } и реализуйте в адаптере EditorUploadInterface::upload().
Модуль примет только PNG, JPEG, GIF и WebP до 10 МиБ и сверит содержимое файла с MIME. Ответ адаптера тоже проверяется: src без // в начале и без управляющих символов, path относительный и без сегментов . и ...
Сервер не даст испортить разметку
Редактор отправляет на сервер только набор операций setInnerHTML и setAttr с 12-символьным хешем исходной версии. Если файл успели поменять, сервер вернёт stale-version и чужую правку не затрёт. Чтобы отключить проверку, адаптер реализует EditorCapabilitiesInterface и возвращает new EditorCapabilities(false). Там же задаётся лимит операций на одно сохранение: по умолчанию 10, допустимо от 1 до 500.
Сервер заменяет только точные куски исходной строки, форматирование остального HTML не меняется. В новую разметку пускают теги текста: абзацы, заголовки, списки, таблицы, ссылки, картинки. script, style, iframe, обработчики on*, srcdoc, PHP-, ASP- и SSI-вставки отклоняются. Старую нестандартную разметку можно оставить как есть, но подменить её новой запрещённой конструкцией нельзя.
Правки внутри <template> и пересекающиеся правки сервер пропускает, остальные изменения из набора сохраняет.
Старым сайтам на windows-1251 или koi8-r перекодировка не грозит: файл остаётся в своей кодировке. Если введённый символ в неё не помещается, сохранение отклоняется. Файлы в UTF-16 править нельзя.
BitrixGPT — только в коробочном Битрикс24
Расширение visualeditor.integration.bitrixgpt связывает кнопку «Спросить ИИ» с чатом BitrixGPT в правой панели. Оно открывает чат, кладёт запрос в черновик сообщения, перед отправкой сохраняет документ и публикует выделенные элементы в ui.page-context. Расширению нужны мессенджер и шаблон Битрикс24, поэтому работает оно только в коробочном Б24. В «Управлении сайтом» модулей im и intranet нет, там остаётся редактор без чата.
Пока идёт внешнее ИИ-задание, документ заблокирован. Цикл ведёт editor.ai: prepare(), finish() и cancel(). Метод resume() восстанавливает блокировку после перезагрузки страницы, если вы сохранили состояние задания. Поздний ответ уже не закроет новый цикл.
Мелочи и находки
- При замене картинки сервер обновляет и lazy-load атрибуты вроде
data-src, иsrcsetу<source>внутри того же<picture>. Браузер не покажет старое изображение. - Ошибки чтения обёртка выводит над редактором в блоке с
role="alert", аdestroy()снимает её подписки.