Перейти к содержанию

Лента событий показывает историю объекта: кто и когда изменил стадию, какие поля заполнил, какие комментарии оставил. Записи лента выводит в том порядке, в каком они переданы, и группирует по дням.

Ленту применяют на странице отдельного объекта — заявки, документа, задания, — когда пользователю нужна хронология изменений и обсуждение рядом с ней.

Расширение только отрисовывает переданные данные и сообщает о действиях пользователя событиями. Запросы к серверу оно не отправляет: обработчики событий пишет разработчик.

В Bitrix Framework за ленту событий отвечает расширение ui.timeline. Оно экспортирует объект Timeline с семью классами:

  • Stream — сама лента: хранит записи, отрисовывает разделы и бросает события.

  • Item — базовый класс записи, от него наследуются все остальные типы.

  • History — запись об изменении объекта, рисует блоки смены стадии и заполненных полей.

  • StageChange и FieldsChange — готовые типы записей для смены стадии и для заполненных полей.

  • Editor — абстрактный редактор над лентой.

  • CommentEditor — редактор комментариев, наследник Editor.

Подключить расширение

Если вы подключаете ленту из PHP, загрузите расширение ui.timeline.

\Bitrix\Main\UI\Extension::load('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.

import { Timeline } from 'ui.timeline';

Классы лежат внутри этого объекта: Timeline.Stream, Timeline.Item и остальные. Вне модульного JavaScript тот же объект доступен как BX.UI.Timeline. Общий порядок подключения расширений описан в статье Расширения.

Собрать ленту событий

Чтобы показать ленту на странице, выполните основные действия:

  1. Создайте экземпляр Stream. Передайте записи в параметре items, а данные об авторах — в параметре users.

  2. Вызовите метод render(). Он собирает разметку ленты и возвращает корневой DOM-узел.

  3. Вставьте этот узел в контейнер на странице.

Пример. Лента из двух записей выводится в узел 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. Базовый класс возвращает пустой массив, поэтому у записи без своих действий кнопки меню нет.

Пример. Метод добавляет записи один пункт меню.

getActions()
{
    return [
        {
            text: 'Открыть документ',
            onclick: this.openDocument.bind(this),
        },
    ];
}

Меню после нажатия на пункт остается открытым. Закройте его в обработчике вызовом 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);
});

Связанные материалы