Лента событий показывает историю объекта: кто и когда изменил стадию, какие поля заполнил, какие комментарии оставил. Записи лента выводит в том порядке, в каком они переданы, и группирует по дням.
Ленту применяют на странице отдельного объекта — заявки, документа, задания, — когда пользователю нужна хронология изменений и обсуждение рядом с ней.
Расширение только отрисовывает переданные данные и сообщает о действиях пользователя событиями. Запросы к серверу оно не отправляет: обработчики событий пишет разработчик.
В Bitrix Framework за ленту событий отвечает расширение ui.timeline. Оно экспортирует объект Timeline с семью классами:
-
Stream— сама лента: хранит записи, отрисовывает разделы и бросает события. -
Item— базовый класс записи, от него наследуются все остальные типы. -
History— запись об изменении объекта, рисует блоки смены стадии и заполненных полей. -
StageChangeиFieldsChange— готовые типы записей для смены стадии и для заполненных полей. -
Editor— абстрактный редактор над лентой. -
CommentEditor— редактор комментариев, наследникEditor.
Подключить расширение
Если вы подключаете ленту из PHP, загрузите расширение ui.timeline.
Вместе с расширением подключаются его зависимости: main.core, main.core.events, main.popup, main.date, main.loader, ui.dialogs.messagebox, ui.design-tokens и ui.fonts.opensans. Загружать их отдельно не нужно.
Если вы работаете в модульном JavaScript, импортируйте объект Timeline из ui.timeline.
Классы лежат внутри этого объекта: Timeline.Stream, Timeline.Item и остальные. Вне модульного JavaScript тот же объект доступен как BX.UI.Timeline. Общий порядок подключения расширений описан в статье Расширения.
Собрать ленту событий
Чтобы показать ленту на странице, выполните основные действия:
-
Создайте экземпляр
Stream. Передайте записи в параметреitems, а данные об авторах — в параметреusers. -
Вызовите метод
render(). Он собирает разметку ленты и возвращает корневой DOM-узел. -
Вставьте этот узел в контейнер на странице.
Пример. Лента из двух записей выводится в узел document-timeline: сначала создание документа, затем смена стадии.
import { Timeline } from 'ui.timeline';
const stream = new Timeline.Stream({
items: [
{
id: 1,
action: 'item_create',
title: 'Документ создан',
createdTimestamp: 1757923200000,
userId: 1,
},
{
id: 2,
action: 'stage_change',
title: 'Стадия изменена',
createdTimestamp: 1757930400000,
userId: 1,
data: {
stageFrom: { id: 1, name: 'Черновик' },
stageTo: { id: 2, name: 'На проверке' },
},
},
],
users: {
1: {
fullName: 'Оператор склада',
link: '/company/personal/user/1/',
photo: null,
},
},
});
document.getElementById('document-timeline').appendChild(stream.render());
Уже созданные узлы метод render() переиспользует и дорисовывает только те записи, которых еще нет в разметке. Поэтому повторный вызов не дублирует содержимое ленты.
Метод getContainer() возвращает корневой узел ленты. Пока render() не вызвали, узла нет и метод возвращает undefined.
Лента состоит из четырех разделов, которые идут сверху вниз: редакторы, прикрепленные записи, запланированные задания и общий список. Общий список лента делит на дни и ставит перед каждым днем разделитель с датой.
Передать параметры ленты
Все параметры конструктора необязательны. Если параметр не передан, лента использует значение по умолчанию.
type StreamOptions = {
items?: Array;
users?: Object;
nameFormat?: string;
pageSize?: number;
tasks?: Array;
editors?: Editor[];
itemClasses?: Array;
};
|
|| Параметр | Тип | Описание ||
|| items | Array | Задает записи общего списка. Для каждого набора данных лента создает запись и добавляет ее в список. По умолчанию пустой массив ||
|| users | Object | Задает данные об авторах записей. Ключ — идентификатор пользователя, значение — объект с полями fullName, link и photo. Ключи, которые не приводятся к положительному числу, лента пропускает. По умолчанию список пуст ||
|| nameFormat | string | Задает формат имени пользователя. Лента передает его каждой записи. По умолчанию значение не задано ||
|| pageSize | number | Задает число записей на страницу. Лента сравнивает с ним длину списка и решает, включать ли подгрузку при прокрутке. Нулевое и отрицательное значение лента заменяет на 20. По умолчанию 20 ||
|| tasks | Array | Задает записи раздела запланированных заданий. По умолчанию пустой массив ||
|| editors | Editor[] | Задает редакторы над лентой. Лента принимает только объекты класса Editor и его наследников, остальные пропускает. По умолчанию список пуст ||
|| itemClasses | Array | Задает свои типы записей массивом пар «код типа — класс». По умолчанию список пуст ||
|#
Данные об авторах лента берет по идентификатору из поля userId записи. Каждое поле объекта пользователя она показывает в своем месте:
-
fullName— всплывающая подсказка аватара, -
link— адрес ссылки на страницу пользователя, -
photo— фон аватара.
Если автора нет в списке users, лента рисует пустую ссылку вместо аватара.
Описать запись ленты
Запись ленты — это объект данных, а не готовый DOM. Лента выбирает по этим данным класс отрисовки, создает запись и хранит ее у себя.
|
|| Поле | Тип | Описание ||
|| id | number | string | Задает идентификатор записи. По нему лента находит запись методами getItem() и getTask() ||
|| action | string | Задает код типа записи. По коду лента выбирает класс отрисовки. По умолчанию пустая строка ||
|| itemClassName | string | Задает имя класса отрисовки строкой, например My.Documents.SignedItem. Лента ищет класс по этому имени и использует его вместо класса из action. Если класса с таким именем нет, лента возвращается к выбору по коду из action. По умолчанию значение не задано ||
|| title | string | Задает заголовок записи. Лента показывает его в верхней строке вместе со временем и аватаром автора и экранирует при выводе. По умолчанию пустая строка ||
|| description | string | Задает содержимое записи. Лента вставляет значение в разметку как HTML. По умолчанию пустая строка ||
|| htmlDescription | string | Задает содержимое записи комментария в HTML. Классы записей об изменениях это поле не выводят. По умолчанию пустая строка ||
|| textDescription | string | Задает содержимое записи без разметки. По умолчанию пустая строка ||
|| createdTimestamp | number | Задает время создания записи в миллисекундах. Лента показывает его в заголовке записи и группирует по нему записи по дням. По умолчанию значение не задано ||
|| userId | number | Задает идентификатор автора записи. По нему лента берет данные из параметра users ||
|| isFixed | boolean | Помечает запись прикрепленной. Лента дополнительно создает копию записи в разделе прикрепленных. По умолчанию false ||
|| data | Object | Задает данные о связанных объектах: стадиях, полях, задании и способе изменения. По умолчанию пустой объект ||
|#
Внимание
Если передать время в секундах, все записи попадут в 1970 год и разделители дней соберутся в один день.
Без положительного значения в этом поле лента не определит день записи. Метод insertItem() в таком случае добавит запись в список, но не покажет ее в разметке.
Внимание
Поля description и htmlDescription лента вставляет в разметку как HTML, без экранирования. Заголовок записи, названия стадий и названия полей она экранирует сама. Если в содержимое записи попадает текст, который ввел пользователь, экранируйте или очищайте его на сервере.
Код в поле action лента сопоставляет с классом отрисовки по своей карте типов.
|
|| Код типа | Класс | Что показывает запись ||
|| item_create | History | Заголовок, время и автора. Подходит для записи о создании объекта ||
|| stage_change | StageChange | Смену стадии и список заполненных полей ||
|| fields_change | FieldsChange | Только список заполненных полей ||
|| comment | Comment | Комментарий с кнопкой раскрытия, вложениями и меню правки ||
|#
Если кода в action нет в карте типов, лента использует класс History.
Поле events в данных записи лента заполняет сама: она подставляет туда свои обработчики нажатия на скрепку, удаления и ошибки. Передавать его не нужно.
Показать изменение стадии и полей
Классы StageChange и FieldsChange берут подробности из объекта data. Заголовки блоков лента подставляет сама: «Изменена стадия» и «Поля заполнены».
|
|| Поле data | Тип | Описание ||
|| stageFrom | Object | Задает стадию до изменения объектом с полями id и name ||
|| stageTo | Object | Задает стадию после изменения объектом с полями id и name ||
|| fields | Array | Задает список заполненных полей. Лента показывает значение title каждого элемента массива ||
|| item | Object | Задает данные объекта до изменения. Метод getItem() возвращает этот объект своему классу записи, готовые классы его не выводят ||
|| task | Object | Задает задание, при выполнении которого появилась запись, объектом с полями id, title и description. Готовые классы записей это поле не выводят: значение доступно своему классу через this.data.task ||
|| scope | string | Задает способ изменения. От него зависит значок записи ||
|#
Блок смены стадии лента рисует, только если заданы обе стадии и их идентификаторы различаются. Если стадия не менялась, запись покажет лишь заполненные поля.
Поле scope принимает одно из четырех значений.
|
|| Значение | Способ изменения ||
|| manual | Пользователь изменил объект вручную. Лента считает способ ручным и когда поле scope не задано ||
|| automation | Объект изменил робот. Такой записи лента ставит значок автоматического действия ||
|| task | Изменение сделано при выполнении задания ||
|| rest | Изменение пришло из приложения через REST ||
|#
Пример. Запись показывает переход между стадиями и два заполненных поля, а значок отмечает, что объект изменил робот.
{
id: 3,
action: 'stage_change',
title: 'Стадия изменена',
createdTimestamp: 1757934000000,
userId: 1,
data: {
scope: 'automation',
stageFrom: { id: 2, name: 'На проверке' },
stageTo: { id: 3, name: 'Утвержден' },
fields: [
{ title: 'Срок хранения' },
{ title: 'Приоритет' },
],
},
}
Добавить свой тип записи
Готовых типов записей четыре: item_create, stage_change, fields_change и comment. Если ни один из них не подходит, напишите свой класс записи и свяжите его с собственным кодом типа.
Написать класс записи
Унаследуйте класс от Timeline.History и переопределите метод renderMain(). Он возвращает DOM-узел с содержимым записи, а заголовок, время и аватар автора лента рисует сама.
Пример. Класс DocumentSigned показывает в записи название документа из данных data.item.
import { Tag, Text } from 'main.core';
import { Timeline } from 'ui.timeline';
class DocumentSigned extends Timeline.History
{
renderMain()
{
const item = this.getItem();
return Tag.render`
<div class="ui-item-detail-stream-content-detail">
${Text.encode(item ? item.name : '')}
</div>
`;
}
}
Класс ui-item-detail-stream-content-detail в примере задает шрифт содержимого записи: размер 13 пикселей и межстрочный интервал 19 пикселей. Без него содержимое возьмет шрифт страницы.
Внутри своего класса доступны методы базовой записи.
|
|| Метод | Описание ||
|| getScope() | Возвращает способ изменения из поля data.scope ||
|| isScopeManual() | Возвращает true, если объект изменили вручную. То же значение метод возвращает, когда поле scope не задано ||
|| isScopeAutomation() | Возвращает true, если объект изменил робот ||
|| isScopeTask() | Возвращает true, если изменение сделано при выполнении задания ||
|| isScopeRest() | Возвращает true, если изменение пришло через REST ||
|| getCreatedTime() | Возвращает дату создания записи. Возвращает null, если поле createdTimestamp не задано или не больше нуля ||
|| formatTime(time) | Форматирует переданную дату в строку времени без даты ||
|| getDataForUpdate() | Возвращает поля записи, которые принимает метод update() ||
|| updateLayout() | Перерисовывает содержимое записи по текущим данным ||
|#
Класс History дает еще три точки для переопределения. Метод renderHeader() рисует верхнюю строку с заголовком, временем и аватаром автора. Методы renderStageChange() и renderFieldsChange() возвращают готовые блоки смены стадии и заполненных полей — их можно вставить в свою разметку вместо того, чтобы собирать заново.
Своей записи можно дать меню действий. Лента строит его на меню из расширения main.popup. Переопределите метод getActions() — он возвращает массив пунктов, у каждого пункта есть текст в поле text и обработчик нажатия в поле onclick. Базовый класс возвращает пустой массив, поэтому у записи без своих действий кнопки меню нет.
Пример. Метод добавляет записи один пункт меню.
Меню после нажатия на пункт остается открытым. Закройте его в обработчике вызовом this.getActionsMenu().close().
Связать класс с кодом типа
Готовый класс лента сама не подхватит: нужно сказать ей, какому коду типа он соответствует. Сделать это можно тремя способами, и выбор зависит от того, кто создает ленту и откуда приходит тип записи.
Первый способ — параметр itemClasses конструктора. Передайте в нем пары «код типа — класс».
Пример. Записи с кодом document_signed лента отрисует классом DocumentSigned.
const stream = new Timeline.Stream({
items: documentHistory,
itemClasses: [
['document_signed', DocumentSigned],
],
});
Внимание
Коды item_create, stage_change, fields_change и comment лента записывает в карту типов после ваших значений, поэтому переопределить их через itemClasses нельзя. Для своей записи выбирайте другой код типа.
Второй способ — событие onAfterInit. Он нужен, когда ленту создает чужой компонент и параметры конструктора вам недоступны. Лента бросает это событие в конце работы конструктора и передает себя в его данных.
Пример. Обработчик добавляет свой тип записи в ленту, созданную чужим кодом.
import { EventEmitter } from 'main.core.events';
EventEmitter.subscribe('BX.UI.Timeline.Stream:onAfterInit', (event) => {
const stream = event.getData().stream;
stream.itemClasses.set('document_signed', DocumentSigned);
});
Подпишитесь на событие до того, как чужой код создаст ленту. Обработчик, добавленный позже, не сработает.
Третий способ — поле itemClassName в данных самой записи. Оно принимает имя класса строкой, и лента находит класс по этому имени в глобальной области видимости. Способ подходит, когда тип записи приходит с сервера вместе с данными.
Управлять записями ленты
Лента хранит записи в трех списках: общий список, прикрепленные записи и запланированные задания. У каждого списка свои методы чтения, а методы изменения принимают уже созданный объект записи.
|
|| Метод | Описание ||
|| createItem(data, itemClassName) | Создает запись по данным и возвращает ее. Второй параметр принимает конструктор класса и отменяет выбор класса по коду типа. Возвращает null, если созданный объект не наследует Item ||
|| addItem(item) | Добавляет запись в конец общего списка и возвращает ленту. Разметку метод не меняет, поэтому после него вызывают renderItems() ||
|| renderItems() | Дорисовывает записи общего списка, которых еще нет в разметке ||
|| insertItem(item) | Вставляет запись в начало ленты с анимацией и добавляет разделитель дня, если для этого дня его еще нет. Подходит для записи, которую пользователь только что создал. Запись с уже занятым идентификатором метод пропускает ||
|| deleteItem(item) | Удаляет запись с анимацией из того списка, где она находится ||
|| pinItem(item) | Добавляет копию записи в раздел прикрепленных с анимацией ||
|| unPinItem(item) | Убирает запись из раздела прикрепленных с анимацией ||
|| getItems() | Возвращает массив записей общего списка ||
|| getItem(id) | Возвращает запись общего списка по идентификатору ||
|| getPinnedItems() | Возвращает массив прикрепленных записей ||
|| getPinnedItem(id) | Возвращает прикрепленную запись по идентификатору ||
|#
Прикрепленная запись — это отдельная копия записи из общего списка с тем же идентификатором. Методы getItem() и getPinnedItem() вернут для одного идентификатора два разных объекта, и обновлять после правки нужно оба.
Пример. Код обновляет обе копии записи после успешного сохранения на сервере.
const item = stream.getItem(commentId);
if (item)
{
item.update(commentData);
}
const pinnedItem = stream.getPinnedItem(commentId);
if (pinnedItem)
{
pinnedItem.update(commentData);
}
Метод update(data) записи принимает только четыре поля:
-
description— текст записи, -
htmlDescription— содержимое записи в HTML, -
data— данные о стадиях, полях и способе изменения, -
userId— идентификатор автора записи.
Метод заменяет те поля, что переданы, и перерисовывает содержимое записи. Заголовок и время записи он не меняет.
При нажатии на скрепку лента переключает раздел прикрепленных сама: меняет признак isFixed записи, добавляет или убирает копию и только после этого бросает событие onPinClick. Обработчику остается сохранить новое состояние на сервере — оно лежит в поле isFixed записи из данных события.
Пример. Обработчик сохраняет новое состояние скрепки.
stream.subscribe('onPinClick', (event) => {
const item = event.getData().item;
BX.ajax.runAction('example.Timeline.updateIsFixed', {
data: {
id: item.getId(),
isFixed: item.isFixed ? 'y' : 'n',
},
});
});
Методы pinItem() и unPinItem() нужны, когда состав прикрепленных записей меняет не пользователь: например, при восстановлении состояния после загрузки данных с сервера. Вызывать их в обработчике onPinClick не нужно — лента уже применила изменение.
Показать запланированные задания
Раздел запланированных заданий лента показывает над общим списком под заголовком «Запланировано». Записи для него передают в параметре tasks конструктора.
Лента хранит эти записи отдельно от общего списка: метод getItems() их не возвращает, копии в разделе прикрепленных для них не создаются. Для чтения используйте методы getTasks() и getTask(id).
Метод updateTasks(tasks) приводит раздел к новому набору данных. Лента сравнивает наборы по идентификаторам записей и действует по-разному:
-
записи, которой нет в новом наборе, — удаляет с анимацией,
-
новую запись — добавляет с анимацией,
-
совпавшую по идентификатору — обновляет через метод
update().
Когда новый набор пуст, лента убирает и заголовок раздела. Вызывайте метод после того, как сервер подтвердил, что состав заданий изменился.
Пример. Раздел запланированных заданий обновляется после ответа сервера.
stream.updateTasks([
{
id: 10,
action: 'item_create',
title: 'Проверить комплектность',
createdTimestamp: 1757937600000,
userId: 1,
},
]);
Подгружать записи при прокрутке
Страницы лента не хранит и не запрашивает. Событием onScrollToTheBottom она только сообщает, что пользователь долистал список до конца.
Обработку прокрутки лента включает сама, если при готовности документа записей в общем списке не меньше значения pageSize. Меньшее число записей лента считает полной историей и обработку не включает. Методы enableLoadOnScroll() и disableLoadOnScroll() включают и выключают ее вручную.
Пока лента в состоянии загрузки, событие не повторяется. Поэтому в обработчике сначала вызывайте startProgress(), а после ответа сервера — stopProgress().
Пример. Обработчик догружает очередную страницу, добавляет записи в конец списка и отключает подгрузку, когда сервер вернул пустую страницу.
stream.subscribe('onScrollToTheBottom', () => {
stream.startProgress();
loadNextPage().then((items) => {
stream.stopProgress();
if (items.length <= 0)
{
stream.disableLoadOnScroll();
return;
}
items.forEach((data) => {
const item = stream.createItem(data);
if (item)
{
stream.addItem(item);
}
});
stream.renderItems();
});
});
Метод startProgress() показывает индикатор загрузки у последней записи списка, stopProgress() убирает его, isProgress() возвращает текущее состояние. Та же пара методов есть у записи — там индикатор появляется внутри ее контейнера.
Добавить редактор над лентой
Редактор — это блок над лентой, через который пользователь создает новые записи. Редакторы передают в параметре editors конструктора, и лента показывает их в самом верху.
Класс Editor абстрактный. Его метод render() бросает ошибку This method should be overridden, поэтому создавать экземпляр Editor напрямую нельзя — наследуйте от него свой класс и переопределите отрисовку.
|
|| Метод | Описание ||
|| getId() | Возвращает идентификатор редактора. Лента использует его ключом в своем списке редакторов ||
|| getTitle() | Возвращает заголовок редактора. В базовом классе не реализован ||
|| render() | Отрисовывает редактор и возвращает DOM-узел. Метод нужно переопределить ||
|| getContainer() | Возвращает контейнер отрисованного редактора ||
|| isRendered() | Возвращает true, если редактор отрисован ||
|| clearLayout(isSkipContainer) | Очищает разметку редактора. Со значением true оставляет корневой контейнер нетронутым ||
|| startProgress() | Показывает индикатор загрузки внутри редактора ||
|| stopProgress() | Убирает индикатор загрузки ||
|#
Параметр id конструктора задает идентификатор редактора. Если передать пустую строку или значение другого типа, редактор подставит случайный идентификатор, и обратиться к нему по имени вы не сможете.
Об ошибках редактор сообщает событием error с текстом в поле message. Лента подписывается на это событие у каждого переданного редактора и выводит текст в консоль браузера. Свое сообщение пользователю покажите в собственном обработчике.
Редактировать комментарии
Класс CommentEditor — готовый редактор комментариев и единственный наследник Editor в расширении. Он рисует поле ввода, подгружает визуальный редактор и бросает события на сохранении. Запросы к серверу он не отправляет, поэтому работа с комментариями складывается из трех частей: подключить редактор, отдать ему разметку визуального редактора и сохранить то, что ввел пользователь.
Подключить редактор комментариев
Конструктор принимает два параметра.
|
|| Параметр | Тип | Описание ||
|| id | string | Задает идентификатор редактора. Пустое значение редактор заменяет случайным ||
|| commentId | number | Задает идентификатор комментария, если редактор правит существующую запись. Значение принимается только числом: строку редактор пропустит. По умолчанию 0 ||
|#
Пример. Редактор комментариев добавлен в ленту первым блоком.
import { Timeline } from 'ui.timeline';
const commentEditor = new Timeline.CommentEditor({
id: 'DocumentCommentEditor',
});
const stream = new Timeline.Stream({
items: documentHistory,
editors: [
commentEditor,
],
});
Загрузить визуальный редактор
Визуальный редактор расширение не содержит: его HTML приходит с сервера. Когда пользователь ставит курсор в поле ввода, редактор бросает асинхронное событие onLoadVisualEditor и показывает индикатор загрузки. Обработчик должен вернуть промис и положить готовую разметку в поле html данных события.
Пример. Обработчик запрашивает разметку визуального редактора и передает ее в данные события.
import { EventEmitter } from 'main.core.events';
EventEmitter.subscribe('BX.UI.Timeline.CommentEditor:onLoadVisualEditor', (event) => {
return new Promise((resolve, reject) => {
BX.ajax.runAction('example.Comment.getVisualEditor', {
data: {
name: event.getData().name,
commentId: event.getData().commentId,
},
}).then((response) => {
event.getData().html = response.data.html;
resolve();
}).catch(reject);
});
});
Сохранить комментарий
Сохранение устроено так же, как загрузка редактора: расширение бросает асинхронное событие и ждет промис. По кнопке отправки редактор проверяет, что текст не пуст, и бросает событие onSave с текстом в поле description, идентификаторами вложений в поле files и идентификатором комментария в поле commentId. Нулевой commentId означает новый комментарий, положительный — правку существующего.
Обработчик сохраняет комментарий и кладет данные новой записи в поле comment данных события. После разрешения промиса редактор очищает поле ввода, бросает событие afterSave и сворачивается. Если промис отклонен, редактор читает поле message данных события и бросает событие error с этим текстом.
Новую запись в ленту редактор не добавляет: создайте ее в обработчике сами.
Пример. Обработчик сохраняет комментарий и вставляет новую запись в начало ленты.
EventEmitter.subscribe('BX.UI.Timeline.CommentEditor:onSave', (event) => {
return new Promise((resolve, reject) => {
BX.ajax.runAction('example.Comment.add', {
data: {
description: event.getData().description,
files: event.getData().files,
},
}).then((response) => {
event.getData().comment = response.data.comment;
const item = stream.createItem(response.data.comment);
if (item)
{
stream.insertItem(item);
}
resolve();
}).catch((response) => {
event.getData().message = response.errors[0].message;
reject();
});
});
});
Событие beforeSave редактор бросает до начала сохранения и передает в нем текст и вложения. Используйте его, чтобы записать черновик или снять метрику.
Показать комментарий в ленте
Готовую запись комментария лента отрисовывает классом Comment. Из объекта Timeline этот класс не экспортируется: лента создает его сама для записей с кодом типа comment.
Комментарий приходит в ленту не полностью. Длинный текст и вложения запись догружает отдельными асинхронными событиями по действию пользователя.
|
|| Событие | Когда срабатывает | Что вернуть обработчику ||
|| onLoadContent | При нажатии кнопки развертывания длинного комментария | Объект в поле comment данных события с готовым HTML в его поле htmlDescription ||
|| onLoadFilesContent | При отрисовке блока вложений | Готовый HTML блока вложений в поле html данных события ||
|| onDelete | После подтверждения удаления в диалоге | Ничего. Достаточно разрешить промис: запись удалится из ленты сама ||
|#
Кнопку развертывания запись показывает, если текст в поле textDescription длиннее 128 символов или к комментарию приложены файлы. Меню записи содержит пункты «Изменить» и «Удалить»: первый переключает запись в режим правки, второй показывает диалог подтверждения из расширения ui.dialogs.messagebox и бросает событие onDelete.
Те же действия доступны из кода методами записи комментария.
|
|| Метод | Описание ||
|| switchToEditMode() | Переключает запись в режим правки и показывает редактор ||
|| switchToViewMode() | Возвращает запись в режим просмотра ||
|| loadContent() | Запускает догрузку полного текста комментария через событие onLoadContent ||
|| loadFilesContent() | Запускает догрузку блока вложений через событие onLoadFilesContent ||
|| getCommendEditor() | Возвращает редактор комментария, создавая его при первом вызове. Имя метода написано с опечаткой в исходном коде расширения ||
|#
События onLoadContent, onLoadFilesContent и onDelete обрабатывают отклоненный промис одинаково: запись берет текст из поля message данных события и бросает событие error.
Подготовить комментарии на сервере
Серверную часть комментариев обслуживают два класса пространства имен Bitrix\UI\Timeline. Через них отвечают на асинхронные события редактора: собирают HTML визуального редактора, разбирают текст и рассылают уведомления об упоминаниях.
Класс Bitrix\UI\Timeline\Comment строит ответы для интерфейса. Конструктор принимает идентификатор объекта пользовательских полей и имя поля для вложений — через них класс находит файлы, приложенные к комментарию.
-
getVisualEditorResponse($name, $id, $text)— собирает компонентbitrix:main.post.formс настроенным набором кнопок и возвращает его. Результат возвращают из действия контроллера, которое вызывает обработчик событияonLoadVisualEditor. -
getFilesContentResponse($id)— собирает компонент показа вложений и возвращает его. Возвращаетnull, если у объекта нет приложенных файлов. -
saveFiles($id, $files)— сохраняет вложения в пользовательское поле объекта и возвращает признак успеха. -
getFileUserFields($id)— возвращает пользовательские поля объекта с файлами. Без установленного модуляdiskвозвращает пустой массив. -
sendMentions($id, $fromUserId, $text, $message, $previouslyMentionedUserIds, $options)— отправляет уведомления пользователям, упомянутым в тексте, и возвращает идентификаторы отправленных сообщений. Автора комментария и ранее упомянутых пользователей метод пропускает, а два последних параметра необязательны.
Класс CommentParser разбирает текст комментария, который пришел из визуального редактора в формате BB-кодов.
-
getHtml($text)— преобразует BB-коды в HTML и раскодирует эмодзи. Результат подходит для поляhtmlDescriptionзаписи. -
getText($text)— убирает из текста всю разметку. Результат подходит для поляtextDescriptionзаписи и для поиска. -
getMentionedUserIds($text)— возвращает идентификаторы пользователей, упомянутых в тексте. -
setUserFields($userFields)— задает пользовательские поля для разбора текста и возвращает сам объект.
Пример. Код разбирает текст комментария и готовит данные записи для ленты.
use Bitrix\UI\Timeline\CommentParser;
$parser = new CommentParser();
$comment = [
'id' => $commentId,
'action' => 'comment',
'htmlDescription' => $parser->getHtml($text),
'textDescription' => $parser->getText($text),
'createdTimestamp' => $createdTime->getTimestamp() * 1000,
'userId' => $userId,
];
Внимание
Разбор текста опирается на модули blog, forum и socialnetwork: класс берет первый установленный из них. Если ни один не установлен, он использует базовый разборщик текста, и часть BB-кодов останется необработанной. Уведомления об упоминаниях уходят только с установленным модулем im, а вложения работают только с модулем disk.
Подписаться на события
Лента и ее части бросают события через EventEmitter из расширения main.core.events. У событий ленты, редакторов и комментариев есть два способа подписки: метод subscribe() на объекте и глобальная подписка по полному имени события.
Пространства имен различаются по классам.
|
|| Класс | Пространство имен ||
|| Stream | BX.UI.Timeline.Stream ||
|| Editor и CommentEditor | BX.UI.Timeline.Editor и BX.UI.Timeline.CommentEditor ||
|| Comment | BX.UI.Timeline.Comment ||
|| Item | UI.Timeline.Item ||
|#
Внимание
Пространство имен записи задано без префикса BX. Для глобальной подписки на событие записи используйте имя UI.Timeline.Item:onPinClick, а не BX.UI.Timeline.Item:onPinClick.
Лента и запись бросают свои события синхронно.
|
|| Событие | Когда срабатывает | Данные события ||
|| Stream:onAfterInit | В конце работы конструктора ленты | stream — созданный объект ленты ||
|| Stream:onAfterRender | После отрисовки ленты | Нет ||
|| Stream:onScrollToTheBottom | При прокрутке до конца общего списка | Нет ||
|| Stream:onPinClick | При нажатии на скрепку записи | item — запись, на скрепку которой нажали ||
|| Item:onPinClick | При нажатии на скрепку конкретной записи | Нет ||
|| Item:onDeleteComplete | После удаления записи из ленты | Нет ||
|| Item:error | При ошибке в записи | message — текст ошибки ||
|#
События редактора и комментария делятся на синхронные и асинхронные. Асинхронное событие ждет промис от обработчика и продолжает работу после его разрешения. Через такие события расширение получает данные с сервера.
|
|| Событие | Тип | Когда срабатывает ||
|| CommentEditor:onLoadVisualEditor | Асинхронное | При первом переходе в поле ввода комментария ||
|| CommentEditor:beforeSave | Синхронное | Перед сохранением комментария ||
|| CommentEditor:onSave | Асинхронное | При сохранении комментария ||
|| CommentEditor:afterSave | Синхронное | После того, как обработчик onSave разрешил промис ||
|| CommentEditor:cancel | Синхронное | При отмене правки и после успешного сохранения ||
|| CommentEditor:error | Синхронное | При ошибке загрузки редактора или сохранения ||
|| Comment:onLoadContent | Асинхронное | При развертывании длинного комментария ||
|| Comment:onLoadFilesContent | Асинхронное | При отрисовке блока вложений ||
|| Comment:onDelete | Асинхронное | После подтверждения удаления комментария ||
|| Comment:error | Синхронное | При ошибке загрузки содержимого или удаления ||
|#
На события конкретного объекта подписывайтесь методом subscribe(): так обработчик получит события только этой ленты.
stream.subscribe('onAfterRender', () => {
stream.getContainer().classList.add('document-timeline--ready');
});
Глобальная подписка нужна для классов, экземпляры которых создает сама лента. Объекта комментария у вас нет, поэтому его события перехватывают по полному имени.
import { EventEmitter } from 'main.core.events';
EventEmitter.subscribe('BX.UI.Timeline.Comment:onDelete', (event) => {
return deleteComment(event.getData().commentId);
});
Связанные материалы
-
Всплывающие окна и меню main.popup — меню записи и всплывающее сообщение о пустом комментарии.
-
Окно сообщения — диалог подтверждения при удалении комментария.
-
Загрузка файлов Uploader — прием файлов от браузера, если вы собираете свой редактор с вложениями.
-
Работа с эмодзи — кодирование и декодирование эмодзи в тексте комментария.
-
Расширения — подключение JavaScript-расширений Bitrix Framework.