Лента событий ui.timeline

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

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

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

В 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);
});

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