Прошло уже два месяца с тех пор, как я написал свою первую строчку кода в 1С, так что невозможно не подвести некоторые итоги и не начать учить программировать других. :)
В течение этого времени почти ежедневно приходилось заниматься поисками решений достаточно стандартных задач сначала в документации (под конец стало понятно, что здесь искать бесполезно), затем в интернете. К моему удивлению, немалое количество вопросов так и осталось без ответа, т.к. найденные решения либо в принципе не работают, либо заданы настолько кусочно-туманно, что приходится долго докапываться, как же это приспособить к конкретному случаю.
На задачу же, которую я хочу разобрать сейчас, на мой взгляд хорошего решения в интернете нет (по крайней мере долгие поиски ничего не дали). Общая постановка задачи такова:
Есть управляемая форма с полем типа "Поле HTML документа". Требуется загрузить в это поле содержимое из макета (общего, или справочника, или отчёта), в котором в некие размеченные места загружаем некие данные и некие объекты.
Чтобы лучше представить себе, для чего это может быть нужно и какую красоту можно сделать, поставим задачу конкретнее:
Сделать приличную начальную страницу конфигурации, содержащую логотип компании и элементы дизайна, приветствие пользователя, его текущие задачи со ссылками на форму, для их обработки.
Вроде бы не сложно, но даже пространная и неплохая статья на Хабре предлагает для вставки в нужные места HTML документа нашего кода использовать какие-нибудь конструкции-метки типа "%БлокСтатусов%" и строковую функцию СтрЗаменить(). Давайте попробуем всё-таки сделать по-человечески.
При веб-разработке в подобных случаях используют операции с DOM-объектами (Document Object Model, объектная модель документа) и обращению к нужным нам элементам по их уникальному ID и/или по их месту в структуре (не в тексте!) документа.
Исходя из того, что в реальной системе на начальной странице для разных пользователей может отображаться разная информация, разный набор отчётов, задач, объектов управления, сделаем её через общую форму и общий макет.
Создаём общую форму произвольного типа с корпоративным синонимом.

Создаём строковый реквизит "HTMLРеквизит", размещаем его на форме, выбрав тип "Поле HTML документа".
"Положение заголовка" укажем "Нет". Скрываем командную панель формы, убирая флаг "Автозаполнение". Можно также убрать рамку поля, указав ей "Цвет формы".
Проверяем в режиме просмотра, и видим такую красоту:

Создаём общий макет "НачальнаяСтраница" типа HTML-документ.

В режиме "Текст" правим HTML-код так, как нам нужно. Например, вставляем в HEAD собственное описание стилей.
В режиме "Редактирование" в начало страницы вставляем шапку и логотип, например, загруженный в общие картинки конфигурации (картинки вставляются через меню Элементы \ Картинка).
Чтобы сразу контролировать результат, как он будет выглядеть в приложении, подключаем вывод начальной страницы. Добавляем одну строчку кода для загрузки макета в реквизит в событие ПриСозданииНаСервере() формы.

&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
HTMLРеквизит = ПолучитьОбщийМакет("НачальнаяСтраница").ПолучитьТекст();
КонецПроцедуры
Подключаем начальную страницу через меню по правой кнопке мыши в дереве конфигурации "Открыть рабочую область начальной страницы". Жмём плюсик "Добавить" и выбираем нашу форму:

Сохраняемся, запускаем приложение, радуемся нашей корпоративной красоте.

Всё? Расходимся? Побежали творить? Да нет же. С этого места суть только начинается, ибо выше показано (как и во всех инструкциях, что я нашёл), как работать с HTML-документом как с текстом, используя строковые функции. А это же объект! И давайте попробуем дальше всё сделать по-человечески, т.е. контейнерно-объектно.
Добавляем в макет контейнеры для вывода текста приветствия и таблицы задач.
<div id="UserHello">Здравствуйте, <span id="UserName"></span>!</div>
<div id="UserTasks">На сегодня для Вас есть задачи:
<div id="TaskList"></div>
</div>
div - это по умолчанию блоковый контейнер, span - строковый (хотя через css можно задать любой стиль отображения). Научимся находить в DOM-дереве HTML-документа нужные нам контейнеры. Искать их будем по ID элемента. Но...
Но, для начала плохие новости: в общем случае никто и ничто не гарантирует, что ID -элементов в HTML-документе будут уникальными. С одной стороны, в рамках одного макета вроде бы всё в наших руках, с другой, стороны легко представить, что HTML-документ будет собираться, например, программно из разных кусков в системе и даже разными людьми, поэтому давайте научимся правильно разруливать такие ситуации и пользоваться DOM-деревом.
И ещё возьмём себе на заметку один нюанс: при обходе документа по DOM-дереву (выполняется рекурсивными алгоритмами) и при текстовом поиске разный порядок нахождения элементов:
Обход по: Тексту DOM
<div ID="One"> 1 1
<div ID="Two"> 2 2
<div ID="Three"> 3 5
<div ID="Four"></div> 4 6
</div>
</div>
<div ID="Three"></div> 5 3
<div ID="Four"></div> 6 4
</div>
Учитывая всё это примем такой простой подход: разбивать наш документ-макет на некие логические блоки, каждому из которых присвоим какой-то человеческий ID. Внутри этих блоков создаём наши контейнеры для вставки, и обращаемся к ним не напрямую, а как к дочерним узлам логических блоков. Так нам будет спокойнее, и, как ни странно, свободнее с именами. И не подумайте, что я призываю делать ID не уникальными. Я призываю соблюдать обычный принцип взрослой разработки: исходим из того, что всё что может произойти - обязательно произойдёт.
Итак, что нам нужно для вывода приветствия пользователя: получить из макета документ как иерархическую структуру, найти в DOM-документа блок "UserHello", в нём блок "UserName", и вставить туда имя пользователя. Документ получить просто:
ДокументHTML = ПолучитьОбщийМакет("НачальнаяСтраница").ПолучитьДокументHTML();
Чтобы получить ссылку на какой-то элемент документа по его (элемента) идентификатору существует метод ПолучитьЭлементПоИдентификатору(), возвращающий первый (по DOM) найденный элемент:
Элемент = ДокументHTML.ПолучитьЭлементПоИдентификатору("UserHello");
Чтобы получить элемент внутри найденного элемента придётся уже колхозить собственную функцию (или свой рекуррентный обход, или, используя стандартный иерархический итератор по дереву DOM):
&НаСервере
Функция НайтиВложенныйЭлемент(ДокументHTML, ТекущийЭлемент, ИщемИдентификатор)
ОбходДерева = ДокументHTML.СоздатьОбходДерева(ТекущийЭлемент);
ТекущийУзел = ОбходДерева.ТекущийУзел;
Пока ТекущийУзел <> Неопределено цикл
ТекущийУзел = ОбходДерева.СледующийУзел();
Если ТекущийУзел.ТипУзла = ТипУзлаDOM.Элемент И ТекущийУзел.Идентификатор = ИщемИдентификатор Тогда
Возврат ТекущийУзел;
КонецЕсли;
КонецЦикла;
Возврат Неопределено;
КонецФункции
После модификации соответствующих элементов в документе нам ещё понадобится функция, возвращающая содержимое DOM-документа как текст. Стандартного свойства или метода у объекта ДокументHTML я не нашёл. Пришлось тоже колхозить:
&НаСервере
Функция ПолучитьТекстHTML(ДокументHTML)
ЗапиcьHTML = Новый ЗаписьHTML;
ЗапиcьHTML.УстановитьСтроку();
ЗаписьDOM = Новый ЗаписьDOM;
ЗаписьDOM.Записать(ДокументHTML, ЗапиcьHTML);
Возврат ЗапиcьHTML.Закрыть();
КонецФункции
Используя всё вышенаписанное (и опуская лишние проверки), код для вывода приветствия получается таким:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ДокументHTML = ПолучитьОбщийМакет("НачальнаяСтраница").ПолучитьДокументHTML();
ВставитьПриветствиеПользователя(ДокументHTML);
HTMLРеквизит = ПолучитьТекстHTML(ДокументHTML);
КонецПроцедуры
&НаСервере
Процедура ВставитьПриветствиеПользователя(ДокументHTML)
БлокПриветствия = ДокументHTML.ПолучитьЭлементПоИдентификатору("UserHello");
// А можно и так:
//БлокПриветствия = НайтиВложенныйЭлемент(ДокументHTML, ДокументHTML, "UserHello");
БлокИмениПользователя = НайтиВложенныйЭлемент(ДокументHTML, БлокПриветствия, "UserName");
ИмяПользователя = ИмяТекущегоПользователя();
Если ИмяПользователя = Неопределено Тогда
БлокПриветствия.ТекстовоеСодержимое = "Просто здравствуйте!";
Иначе
БлокИмениПользователя.ТекстовоеСодержимое = ИмяПользователя;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ИмяТекущегоПользователя()
Возврат "Вася";
КонецФункции
Творим дальше. Рассмотрим как сделать таблицу задач пользователя с обработкой кликов по ссылкам и запуском форм.
Создаём простейший справочник "Задачи". Задачам добавим единственный реквизит "Статус" типа "Перечисление". Делаем выборку задач и грузим в таблицу.
Создать таблицу в DOM не сложно, и по этой теме в интернетах есть множество примеров с разной степенью детализации. Проблема в том, что опять-таки в них применяется либо "текстовый" подход, когда используют HTML-код как текст, либо DOM-подход, но тогда задание каждого свойства элемента (а их иногда не мало) - это одна или больше строка кода. Чтобы создавать элемент через DOM, но в одну строчку, давайте набросаем простенькую функцию, и будем передавать ей нужные свойства элемента в виде структуры.
&НаСервере
Функция СоздатьHTMLЭлемент(ДокументHTML, РодительскийЭлемент, ТегЭлемента, СвойстваЭлемента = Неопределено)
Элемент = ДокументHTML.СоздатьЭлемент(ТегЭлемента);
Если СвойстваЭлемента <> Неопределено Тогда
Для каждого Свойство Из СвойстваЭлемента Цикл
Элемент[Свойство.Ключ] = Свойство.Значение;
КонецЦикла;
КонецЕсли;
РодительскийЭлемент.ДобавитьДочерний(Элемент);
Возврат Элемент;
КонецФункции
Теперь разберёмся со ссылками в документе. Ссылки на внешние сайты можно вставлять точно так же, как и в обычном HTML, хотя открываются они в окне основного приложения, что не всегда удобно. Нам же нужны ссылки, при нажатии на которые мы будем делать что-то в 1С, например, открывать форму справочника "Задачи". Для этого придётся писать программную обработку клика по полю HTML-документа. Процедуре обработки клика передаётся объект ДанныеСобытия, в котором есть ссылка на "кликнутый" элемент со всеми его свойствами. Так что у нас есть выбор, как мы будем хранить в ссылке нужную нам для дальнейшей обработки клика информацию - можем в ID ссылки, можем в className, можем как-нибудь ещё. В нашем примере используем ID, куда мы запишем код задачи. Поскольку в стандартах HTML ID должен начинаться с букв, дописываем в начало какие-то буквы. Поехали:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ДокументHTML = ПолучитьОбщийМакет("НачальнаяСтраница").ПолучитьДокументHTML();
ВставитьПриветствиеПользователя(ДокументHTML);
ВставитьЗадачиПользователя(ДокументHTML);
HTMLРеквизит = ПолучитьТекстHTML(ДокументHTML);
КонецПроцедуры
&НаСервере
Процедура ВставитьЗадачиПользователя(ДокументHTML)
БлокЗадач = ДокументHTML.ПолучитьЭлементПоИдентификатору("UserTasks");
БлокТаблицыЗадач = НайтиВложенныйЭлемент(ДокументHTML, БлокЗадач, "TaskList");
ЗадачиПользователя = ПолучитьЗадачиПользователя();
Если ЗадачиПользователя.Количество() = 0 Тогда
БлокЗадач.ТекстовоеСодержимое = "Счастливчик! Задач на сегодня нет. Ступайте домой!";
Иначе
//Строим красивую таблицу задач
Таблица = СоздатьHTMLЭлемент(ДокументHTML, БлокТаблицыЗадач, "table", Новый Структура("className", "TasksTable"));
Для каждого Задача Из ЗадачиПользователя Цикл
СтрокаТаблицы = СоздатьHTMLЭлемент(ДокументHTML, Таблица, "tr");
СоздатьHTMLЭлемент(ДокументHTML, СтрокаТаблицы, "td", Новый Структура("ТекстовоеСодержимое, className", Строка(Задача.КодЗадачи), "TaskCode"));
СоздатьHTMLЭлемент(ДокументHTML, СтрокаТаблицы, "td", Новый Структура("ТекстовоеСодержимое", Задача.Название));
СоздатьHTMLЭлемент(ДокументHTML, СтрокаТаблицы, "td", Новый Структура("ТекстовоеСодержимое, className", Строка(Задача.Статус), "TaskStatus" + Перечисления.СтатусЗадачи.Индекс(Задача.Статус)));
ЯчейкаСсылки = СоздатьHTMLЭлемент(ДокументHTML, СтрокаТаблицы, "td");
СоздатьHTMLЭлемент(ДокументHTML, ЯчейкаСсылки, "a", Новый Структура("ТекстовоеСодержимое, className, id, href", "перейти", "TaskLink", "Link" + Задача.КодЗадачи, "#"));
КонецЦикла;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ПолучитьЗадачиПользователя()
ЗадачиПользователя = Новый Массив;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Задачи.Код КАК КодЗадачи,
| Задачи.Наименование КАК Название,
| Задачи.Статус КАК Статус
|ИЗ
| Справочник.Задачи КАК Задачи
|УПОРЯДОЧИТЬ ПО
| Задачи.Статус.Порядок,
| Задачи.Наименование";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
ЗадачиПользователя.Добавить(Новый Структура("КодЗадачи, Название, Статус", Выборка.КодЗадачи, Выборка.Название, Выборка.Статус));
КонецЦикла;
Возврат ЗадачиПользователя;
КонецФункции
Чтобы навести красоту, поправим стили в макете:
Запускаем, радуемся:

Осталось написать код, делающий наши ссылки работоспособными. Создаём обработчик событий нажатия нашего поля на форме. Это первая и единственная процедура, которая создаётся на клиенте. Всё остальное на сервере!

Процедура простая и понятная. Чтобы открыть форму справочника на нужной нам записи в качестве единственного параметра в структуре параметров передадим "ТекущаяСтрока" в которую запишем ссылку на задачу (не код задачи, а ссылку!).
&НаКлиенте
Процедура HTMLРеквизитПриНажатии(Элемент, ДанныеСобытия, СтандартнаяОбработка)
//Обрабатываем только наши ссылки. Остальные работают как обычные
Если ДанныеСобытия.Element.className = "TaskLink" Тогда
СтандартнаяОбработка = Ложь;
ИдентификаторСсылки = ДанныеСобытия.Element.id;
//Убираем наши буквы "Link" в начале ID ссылки
КодЗадачи = Сред(ИдентификаторСсылки, 5);
ЗадачаСсылка = СсылкаНаЗадачу(КодЗадачи);
Если ЗадачаСсылка = Неопределено Тогда
Сообщить("Задача не найдена!");
Иначе
ПараметрыОткрытияФормы = Новый Структура ("ТекущаяСтрока", ЗадачаСсылка);
ОткрытьФорму("Справочник.Задачи.ФормаСписка", ПараметрыОткрытияФормы);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция СсылкаНаЗадачу(КодЗадачи)
Справочник = Справочники.Задачи;
ЗадачаСсылка = Справочник.НайтиПоКоду(КодЗадачи, Истина);
Если ЗадачаСсылка <> Справочник.ПустаяСсылка() Тогда
Возврат ЗадачаСсылка;
КонецЕсли;
КонецФункции
Вот, собственно, и всё. Приношу свои извинения тем, кому изложенное показалось очевидным и неинтересным. Повторюсь, написал это как памятку, в связи с тем, что в интернете нет примеров работы с HTML-документом в 1C как с DOM (можете сами поискать по названию использованных функций). Для работы используется 1С:Предприятие 8.3, учебная версия (8.3.27.1688). Выгрузка прилагается.
Если что-то сделал не так, или не по 1С-стилю, буду признателен за конструктивные указания - это моя первая публикация на сайте, а мой опыт работы с 1С всего два месяца.
Вступайте в нашу телеграмм-группу Инфостарт