Итак, как я и обещал, согласно собственного плана на данный цикл статей, в сегодняшней я расскажу о своем решении проблемы отсутствия штатных средств пользовательского ввода-вывода данных в макросах, для редакторов "Р7/OnlyOffice".  Оно не претендует на какую-либо уникальность, или универсальность но, по крайней мере на мой взгляд, достаточно простое и удобно, чтобы рассказать  вам о нем.

  1. Введение в проблемы написания макросов и плагинов в «Р7»

  2. Средства взаимодействия с пользователем в макросах через  АПИ «Р7»

  3. Мое собственное решение для построения сложных форм в макросах (вы сейчас здесь)

  4. Почти автоматическая генерация плагинов

  5. Прочее (по результатам  этих статей)

Кто читал первые две статьи данного цикла, могут пропустить данное введение, а остальным вкратце поясню краткое содержание, приведшее в итоге к созданию плагина, о котором я сегодня расскажу.

Итак, часто задачи автоматизации офисных приложений, подразумевают некоторое взаимодействие автоматизируемой программы с пользователем. И так исторически сложилось, что для этого нет прямых методов в АПИ макросов  редактора "Р7/OnlyOffice". Но благодаря тому, что можно получить доступ к некоторым методам внутреннего АПИ редакторов, частично этот вопрос можно обойти, принуждая редактор выводить на экран заготовку окна (или запрограммированные  уже заранее информационные окна). Так же данную заготовку можно наполнять своим внутренним содержанием. Таким образом, можно создавать модальные (или нет) окна, в которых можно как выводить, так и вводить информацию. В принципе, на этом можно было бы, и остановиться,  раз я  донёс до вас этот способ, пойти себе дальше заниматься своими делами… А вы -  мучайтесь в редакторе макросов над созданием окон с  более – менее сложным содержанием самостоятельно!

Краткая предистория излогаемого решения, если кому то она интересна

Так исторически сложилось, что я работал над совершенно другим плагином,  по сути – генератором, создающим заготовки плагинов, о котором позже расскажу. В ходе работы над ним, у меня возникла идея, что удобно было бы создавать последовательный набор форм, в которых пользователь должен сделать выбор из списка или прямой ввод определённых параметров для генерируемого плагина. В добавок ко всему, в зависимости от выбранных параметров, может  происходить ветвление к разным «веткам» графа всех форм. И хорошо было  бы предусмотреть ещё  возврат на предыдущие этапы. В общем – оказался нужен пользовательский паттерн, хорошо известный как Мастер (или по английский - Wizard). Я немного порылся в интернете,  и не найдя ничего для себя подходящего, не без помощи ИИ, создал свой вариант такого Мастера. Основной класс я назвал просто: Wizard. Он реализовывает автоматическую генерацию всех  запланированных форм внутри  HTML контейнера (обычно это div заданного размера). Так  же класс  осуществляет всю требуемую навигацию согласно графу переходов, собирает данные вводимые  на каждом шагу и т.д.  Чтобы не мучиться с ручным написанием структуры всего наполнения и переходов между экранами, я написал отдельный редактор,  который на выходе создаёт JSON объект с полным описанием всей необходимой для последующей работы Wizard-a информацией. В итоге,  я уже почти  дошел написал плагин, ради которого я затеял всю эту историю с Wizard, но по ходу работы над ним понял, что сама по себе идея может быть упрощена до состояния, которое позволит вставлять сгенерированные  формы (или одну форму) в модальное окно в макросе "Р7"!  Благо, вся кухня генерации формы, вводу данных в её полях,  валидации этих данных и сбору данных в одном месте после завершения ввода, по факту уже имелась в Wizard-е. Но тащить все дополнительные возможности этого класса ради одной формы это был бы явный перебор! В итоге я упростил и доработал исходный класс Wizard, и назвал новый класс -  WindowForm. В первой версии оказались некоторые архитектурные изъяны, и я сделал вторую версию, которую чтобы не путаться, я назвал WindowForm2. Он то и лежит в основе всего того, о чем я хочу сегодня рассказать.

PS. Wizard, тем не менее, так же вполне себе работает в макросах. Я ради интереса там опробовал и его, и вполне остался им доволен! Но, поскольку потребность в таких сложных режимах ввода данных, как «Мастер»,  в макросах встречается куда реже, его интеграцию я отложил до лучших времен.

Предварительное краткое пояснение сложности предлагаемого решения для макросов

Если кому было интересно, и он прочел спойлер то, казалось бы, достаточно  добавить класс который генерирует формы, в любой макрос, и на этом можно было бы успокоиться! Но в таком случае, проблемой оказалось бы то, что сам по себе класс WindowForm2 содержит большое число настроек, которые он  изначально наследует  от более сложного исходного класса Wizard. А упростить  код и избавится от «лишнего» –  тоже означало «стрелять  себе в ногу», так как все эти настройки могут оказаться весьма полезными в разных сценариях  работы с данными формы. Для инициализации объекта класса  конечному пользователю все эти настройки нужно хотя бы знать. Но изучать что-то дополнительное, ради простой формы ввода в наше стремительное время, и лень, и чаще всего избыточно. Плюс к этому, есть достаточно много типовых сервисных функций которые хорошо было-бы добавить, чтобы упростить использование форм в "Р7/OnlyOffice". Можно конечно  дополнить и  сам WindowForm2, но не хотелось! Он пишется как достаточно универсальный проект, который можно использовать и вне редакторов, скажем  в браузере для других  моих проектов, с офисными редакторами никак не связанными.

В итоге был создан сервисный класс FormMacroManager, в котором дописана бОльшая часть работы по настройке самого класса генератора WindowForm2, и обработка результатов после закрытия формы, именно в разрезе использования в редакторах "Р7/OnlyOffice". В сумме, для дальнейшей работы в плагине этих классов было бы вполне достаточно! Но для макросов и этого мне показалось мало! Код классов достаточно объемный для макросов и если его оставить как есть, то он будет занимать там слишком много места! Поэтому были сделаны ещё несколько очень простых сервисных функций и один класс, которые позволяют «упаковать» код двух  основных классов и все настройки формы в раздельные макросы. Потом это всё в нужном порядке вызывается определённым образом в отдельном, главном макросе. Это дает пользователю сосредоточится на небольшом числе функций, в которых он реализует  свои непосредственные задачи при использовании формы.

Вкратце архитектура вышла такой:

Создание форм с помощью класса WindowForm2

Я не стану расписывать архитектурные решения или программистские находки для этого класса, так как они для данной статьи  и не нужны! Просто немного распишу его возможности, поскольку  хотя они и скрыты в плагине, но доступны в макросах, и при необходимости почти всех их можно в добавить и  использовать для своих целей, когда возможностей по дефолтным настройкам в  FormMacroManager окажется мало. Более подробное и полное описание доступно в плагине, и вызывается через кнопку «Помощь» в  разделе «WindowForm2» меню плагина. Сам класс получился с довольно  большими возможностями. Вот основные:

  • Создание форм на базе своего или встроенного html шаблона разметки.

  • Кастомизация внешнего вида, через описание в стиле CSS, как отдельных блоков формы (заголовок, подсказки и т.п.) так и отдельных типов полей.

  • Разнообразные типы полей как стандартные (текст, телефон, email, списки, флажки и т.п.) так и специфичные, такие как: выбор файла (возвращает путь к файлу)или  кастомное поле, к которому можно в виде параметра добавлять html разметку, что позволяет серьёзно расширять возможности содержания поля.

  • Возможность автоматической разметки полей формы в несколько столбцов (до 5). Для этого достаточно указать номер столбца  размещения поля, отличный от 1. Необходимые расчёты класс берет на себя. Так же можно задавать  и colspan, создавая более логичные размеры для полей требующих большего пространства на форме.

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

  • Использование системы пользовательских функций-хуков на события, которые можно добавлять  к любому из полей отдельно. Их можно использовать, к примеру, для генерации значения для любого другого поля с учетом данных введенных в текущее поле. В целом хуки делятся на:

    • кастомные валидаторы (синхронные и асинхронные), которые можно применить как к одному, так и к нескольким полям формы;

    • обработчики событий полей, например таких как "onChange";

    • колбэки жизненного цикла формы,  которые служат для контроля глобальных событий формы, таких как «закрытие формы», или «ошибка валидации», не через отдельное программирование в макросах, а через общую настройку в параметрах формы, что позволяет переносить их вместе с JSONобъектом описания формы между документами;

    Вообще вся эта система хуков, может использоваться даже как система «плагинов», серьезно расширяющая гибкость использования форм, и примеры такой организации есть в документации к классу.

  • Координаты для вывода. Причем речь идет как о прямых координатах, таких как позиция  (x,y) для экспорта в pdf, так и о координатах типа столбец/строка для табличного вывода, или «Лист!A1» для таблицы Excel, или имя закладки для документов Word. Их можно использовать для прямого экспорта введенных данных в документ. Причем, координаты можно задавать всех 4 типов одновременно, а использовать только необходимые, уже по месту с используемым  редактором.

  • Локализация описания полей и формы. Можно создавать формы сразу на нескольких языках, и переключать язык формы при необходимости.

  • Всплывающие подсказки. Помимо описания для каждого поля, и встроенной подсказки что поле для ввода обязательное, можно выборочно создавать всплывающие подсказки. Например, с целью более развернутого пояснения, что ожидается от вводимых данных.

  • Режим просмотра набора внешних данных. Можно передавать внутрь формы предопределённый набор данных и установив режим просмотра в форме, просматривать эти данные в более удобном виде, нежели  в виде таблицы или JSON структуры. Причем режим просмотра так же можно дополнять различными возможностями, такими как: просмотр в режиме read-only; использовать режим предустановки данных с возможностью изменения в полях и т.д.

  • Множество режимов экспорта введенных данных при окончании ввода и закрытия непосредственно самой формы, с различной степенью детализации (например, с координатами для последующего экспорта в документ и т.п.)

  • Возможность использовать глобальные события для формы в целом, таких как «on», «off», что дает в ряде случаев контролировать форму на уровне событий DOM, а не только алгоритмически. Но это скорее полезно вне макросов и плагинов.

В общем, даже из этого краткого списка видно, что возможности WindowForm2 в целом будут избыточными для подавляющего большинства сценариев использования в макросах.  Но если все же они будут  нужны, то большую часть можно реализовать сравнительно несложными методами, которые я уже предусмотрел и описал в руководстве по классу (напоминаю - оно доступно в плагине в меню «Помощь/ WindowForm2», причем сразу  с примерами некоторых хитрых сценариев).
В общем, как по мне, класс вышел достаточно интересным, и универсальным. Вдобавок, он не тащит за собой никаких дополнительных библиотек. И даже не требует отдельного файла CSS для кастомизации, так как это повлекло бы определённые проблемы при попытке применять такие стили в рамках макросов в "Р7". Напоследок вот скрин простейшего использования этого класса в браузере:

Код примера
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Пример WindowForm2</title>
</head>
<body>
    <div id="wizard-container"></div>

    <script src="WindowForm2.js"></script>
    <script>
        // Конфигурация простой формы
        const formConfig = {
            title: 'Простая демо форма',
            description: 'Форма сгенерирована для демонстрации использования класса WindowForm2 в браузере',
            fields: [
                {
                    name: 'name',
                    type: 'text',
                    label: 'Имя',
                    placeholder: 'Введите ваше имя',
                    required: true
                },
                {
                    name: 'email',
                    type: 'email',
                    label: 'Email',
                    placeholder: 'Введите ваш email',
                    required: true
                },
                {
                    name: 'age',
                    type: 'number',
                    label: 'Возраст',
                    min: 1,
                    max: 120
                },
                {
                    name: 'gender',
                    type: 'radio',
                    label: 'Пол',
                    options: [
                        { label: 'Мужской', value: 'male' },
                        { label: 'Женский', value: 'female' }
                    ]
                },
                {
                    name: 'comment',
                    type: 'textarea',
                    label: 'Комментарий',
                    placeholder: 'Введите ваш комментарий',
                    rows: 3
                }
            ]
        };

        // Инициализация формы
        const form = new WindowForm2();
        form.initFromObject(formConfig);
    </script>
</body>
</html>

Класс FormMacroManager

FormMacroManager -  это класс для управления формами WindowForm2 при работе с ними в  редакторах "Р7/OnlyOffice".

Причины создания этого класса я уже описывал:

  1. Некоторая избыточность и сложность при создании и  полноценной работы  формы в WindowForm2.

  2. Для вывода формы в макросах "Р7/OnlyOffice" требуется использования хака с Common.UI.Window (смотри предыдущую статью цикла), а значит много запутанного и сложного кода.

Конечно, можно было бы и видоизменить исходный класс, но он и так стал большим и вдобавок, потерял бы тогда гибкость. Поэтому я создал класс  FormMacroManager   как своеобразный мост, который бы скрывал в себе всю черновую работу по созданию модальных окон в редакторах и упрощал пользователю работу со всеми требуемые внутренними механизмами форм WindowForm2. Заодно, этот класс  решет вопросы на случаи либо экспорта данных (ведь формы обычно для этих целей и создаются), либо импорта данных из таблиц, когда их нужно просмотреть их в удобном виде  формы  с пагинацией.

Основные возможности класса:

  • Два режима работы: ввод и просмотр данных

  • Fluent interface для простой конфигурации (последовательный вызов методов класса через точку друг за другом)

  • Экспорт данных в табличный или текстовый документ по записанным в полях “координатам”

  • Гибкая система стилизации

  • Валидация данных и обработка ошибок

  • Интеграция с WindowForm2

  • Загрузка данных из диапазонов ячеек с поддержкой пагинации (только для данных из таблиц)

  • Генерация форм по шапке таблицы в режиме просмотра

  • Навигация между записями в режиме просмотра

Помимо этого, при создании класса можно указывать параметры которые непосредственно нужны для генерации формы в макросах. Например – размеры окна и колбэк функции, которые вызываются в те или иные моменты работы с формой (например, перед считыванием очередной порции данных, или при закрытии формы). Подробное описание данного класса я тоже приводить не буду, ибо оно так же есть с примерами в плагине «ФормаЛаб».

Особо хотел бы отметить возможность в табличных документах простого способа создавать формы  из данных шапки таблицы, с последующей автоматической загрузкой данных, содержащихся в строках ниже шапки, в постраничном режиме. Это может избавить от необходимости создавать формы, в случае необходимости быстрого просмотра  табличных наборов данных в режиме карточек. В Про версии «ФормаЛаб» можно так же предварительно сканировать такие таблицы, а потом  изменять сгенерированную форму с возможностью  кастомизации, и устанавливать режим просмотра  с модификацией данных и экспортом в исходный документ.

// Создание формы ввода на основе таблицы заказов
const formManager = new FormMacroManager({
    mode: 'input',
    windowTitle: 'Новый заказ',
    exportFormat: 'excel'
});

await formManager.createFormFromTableHeader('A1:H1', {
    title: 'Форма нового заказа (автогенерация)',
    description: 'Все поля созданы автоматически по заголовкам таблицы',
    sheetIndex: 0,
    autoCoordinates: true,
    requiredFields: ['Клиент', 'Email клиента'],
    selectOptions: {
        'Статус': [
            { value: 'new', label: 'Новый' },
            { value: 'processing', label: 'Обработка' },
            { value: 'sent', label: 'Отправлен' },
            { value: 'delivered', label: 'Доставлен' }
        ],
        'Способ доставки': [
            { value: 'courier', label: 'Курьер' },
            { value: 'pickup', label: 'Самовывоз' },
            { value: 'mail', label: 'Почта России' }
        ]
    }
});

await formManager.initialize();
await formManager.showWindow();

Собираем вместе. Плагин «ФормаЛаб»

Конечно, можно было бы просто предложить набор из описанных классов конечным пользователям. И показать на примерах, как ими можно пользоваться в составе макросов. Но получается  опять неудобно, ибо нужно будет  каким-то образом решать следующие задачи для каждого нового документа:

  1. Добавить код классов внутрь макросов

  2. Произвести все требуемые действия по созданию JSON объекта, описывающего форму (исключая описанный выше способ автогенерации формы по шапке таблицы, которые к тому же работает только в режим просмотра и только для табличных документов)

Я пока редактировал и отлаживал код этих классов, в полной мере ощутил всё неудобство такого подхода, поэтому решил, что лучшим способом использования созданных классов будет создание плагина, который бы позволял более-менее просто создавать формы. Так же этим плагином нужно  внедрять весь необходимый для последующей работы код в документ. При этом,  выделяется  по сути один макрос с которым пользователь работает как с программой– точка создания и показа формы, а все остальные сопутствующие скрипты  - раскидываются по другим, специализированным макросам. Может показаться, что я усложнил схему. Для разовых случаев, безусловно, это так и есть! Но если речь идет о вспомогательном коммерческом решении, то сделано это осознано,  для того, чтобы бы произошла декомпозиция кода и данных, которая  к тому же позволяет не лезть особо в код самих классов.

Как внутренне устроен плагин я так же не стану расписывать. Сегодня я немного про пиар,  а не про программирование.

Итак, допустим, вы поставили себе такой плагин и его запустили:

Плагин в целом простой в использовании

Визуально он  состоит из: области меню плагина (1) в верхней части редактора, области служебных кнопок внизу плагина (2), и в центральной области разделенный на три части непосредственно сам редактор форм (границы частей можно менять перетаскиванием мышкой).

В левой половине отображается дерево структуры формы (3),  которое либо показывает простую структуру формы и её полей, либо структуру JSON файла, её описывающую.

В центральной части редактора (4)визуальное представление формы (без привязки к истинных  размеров конечной формы!). Причем, отмечу, эта часть редактора представляет собой объект класса WindowForm2!

В правой части (5) представлен редактор параметров (полей структуры) описывающие выбранный узел  в дереве структуры (3). Это поле так же является  объектом класса WindowForm2! При выборе поля, оно автоматически подсвечивается в центральной части редактора:

Вы можете свободно добавлять (через меню «Правка/Добавить поле)» или удалять поля в форме, выбирать тип поля, устанавливать различные параметры в зависимости от выбранного типа поля и т.п. Для добавления нового поля нужно нажать кнопку «Обновить форму»!

Как видно из скрина, вы можете поменять большинство параметров (но не все!)  доступных в настройках  полей в WindowForm2. Часть не самых востребованных, чтобы не загромождать область редактирования, просто не добавлены (варианты переводов, тултипы и т.п.).  

Можно задать позицию в сетчатом представлении формы (не требуется обновлять через кнопку!):

Есть набор предустановленных шаблонов форм. Его можно использовать как основу для создания форм под себя, а не «с нуля»,  с некоторым типовым набором полей, зависящих от типа задачи:

Шаблон применяется сразу и  затирает ранее редактируемую  форму, поэтому будьте осторожны с его применением!

Пример описания формы в JSON формате
{
  "title": "Ввод ФИО",
  "description": "Форма для ввода фамилии, имени и отчества",
  "fields": [
    {
      "id": "field_1762955147532_0",
      "position": {
        "row": 1,
        "col": 1,
        "colSpan": 2,
        "rowSpan": 1
      },
      "name": "lastName",
      "type": "text",
      "label": "Фамилия",
      "required": true,
      "placeholder": "Введите фамилию",
      "defaultValue": ""
    },
    {
      "id": "field_1762955147532_1",
      "position": {
        "row": 2,
        "col": 1,
        "colSpan": 1,
        "rowSpan": 1
      },
      "name": "firstName",
      "type": "text",
      "label": "Имя",
      "required": true,
      "placeholder": "Введите имя"
    },
    {
      "id": "field_1762955147532_2",
      "position": {
        "row": 2,
        "col": 2,
        "colSpan": 1,
        "rowSpan": 1
      },
      "name": "middleName",
      "type": "text",
      "label": "Отчество",
      "required": false,
      "placeholder": "Введите отчество (при наличии)"
    }
  ],
  "layout": {
    "type": "grid",
    "columns": 2,
    "gap": "15px",
    "rowHeight": "auto"
  }


Есть набор дизайнов формы. Он не сильно большой, но все-таки позволят делать формы менее скучными внешне:

Пример описания стиля элементов формы в JSON формате
{
  "container": {
    "width": "-webkit-fill-available",
    "margin": "0 auto",
    "padding": "32px",
    "fontFamily": "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
    "backgroundColor": "#f8f9fa",
    "borderRadius": "20px",
    "boxShadow": "0 2px 10px rgba(0,0,0,0.1)",
    "border": "none",
    "background": "#f8f9fa",
    "backdropFilter": "none",
    "WebkitBackdropFilter": "none",
    "color": "#1f2937",
    "borderColor": "#d1d5db"
  },
  "form_container": {
    "flex": "1 1 auto",
    "overflow": "auto"
  },
  "header": {
    "marginBottom": "32px",
    "textAlign": "center"
  },
  "title": {
    "fontSize": "28px",
    "fontWeight": "600",
    "color": "#5a5a5a",
    "margin": "0 0 8px 0",
    "lineHeight": "1.2",
    "textShadow": "2px 2px 4px rgba(255, 255, 255, 0.8), -2px -2px 4px rgba(0, 0, 0, 0.1)"
  },
  "description": {
    "fontSize": "16px",
    "color": "#7a7a7a",
    "margin": "0",
    "lineHeight": "1.4",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "form": {
    "marginBottom": "32px"
  },
  "formGrid": {
    "marginBottom": "32px",
    "display": "grid",
    "gridAutoRows": "auto",
    "alignItems": "start",
    "gap": "24px"
  },
  "fieldGroup": {
    "marginBottom": "24px",
    "position": "relative",
    "backgroundColor": "#e6e7ee",
    "borderRadius": "16px",
    "padding": "20px",
    "boxShadow": "8px 8px 16px #d1d3db, -8px -8px 16px #ffffff",
    "border": "none"
  },
  "fieldGroupGrid": {
    "marginBottom": "16px",
    "position": "relative",
    "padding": "8px",
    "backgroundColor": "#e6e7ee",
    "borderRadius": "12px",
    "boxShadow": "6px 6px 12px #d1d3db, -6px -6px 12px #ffffff"
  },
  "label": {
    "display": "block",
    "marginBottom": "8px",
    "fontWeight": "500",
    "color": "#1f2937",
    "fontSize": "14px",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "requiredMark": {
    "color": "#d63031",
    "marginLeft": "4px"
  },
  "input": {
    "width": "100%",
    "padding": "12px 16px",
    "border": "none",
    "borderRadius": "12px",
    "fontSize": "16px",
    "boxSizing": "border-box",
    "backgroundColor": "#ffffff",
    "color": "#1f2937",
    "transition": "all 0.3s ease",
    "outline": "none",
    "boxShadow": "inset 4px 4px 8px #d1d3db, inset -4px -4px 8px #ffffff",
    "fontFamily": "inherit",
    "borderColor": "#d1d5db"
  },
  "select": {
    "width": "100%",
    "padding": "12px 16px",
    "border": "none",
    "borderRadius": "12px",
    "fontSize": "16px",
    "boxSizing": "border-box",
    "backgroundColor": "#ffffff",
    "color": "#1f2937",
    "transition": "all 0.3s ease",
    "outline": "none",
    "boxShadow": "inset 4px 4px 8px #d1d3db, inset -4px -4px 8px #ffffff",
    "fontFamily": "inherit",
    "borderColor": "#d1d5db"
  },
  "checkbox": {
    "marginRight": "12px",
    "transform": "scale(1.1)",
    "boxShadow": "2px 2px 4px #d1d3db, -2px -2px 4px #ffffff"
  },
  "radioGroup": {
    "display": "flex",
    "flexDirection": "column",
    "gap": "12px"
  },
  "radioItem": {
    "display": "flex",
    "alignItems": "center",
    "gap": "10px"
  },
  "radioLabel": {
    "fontWeight": "normal",
    "marginLeft": "0",
    "cursor": "pointer",
    "color": "#1f2937",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "checkboxLabel": {
    "fontWeight": "normal",
    "marginLeft": "0",
    "cursor": "pointer",
    "color": "#1f2937",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "checkboxItem": {
    "display": "flex",
    "alignItems": "center",
    "marginBottom": "12px"
  },
  "fieldDescription": {
    "fontSize": "13px",
    "color": "#6b7280",
    "marginTop": "6px",
    "fontStyle": "italic",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "error": {
    "color": "#d63031",
    "fontSize": "13px",
    "marginTop": "6px",
    "display": "none"
  },
  "errorContainer": {
    "backgroundColor": "#fceaea",
    "color": "#d63031",
    "padding": "12px 16px",
    "borderRadius": "12px",
    "marginBottom": "24px",
    "display": "none",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff"
  },
  "navigation": {
    "width": "97%",
    "display": "flex",
    "position": "absolute",
    "bottom": "8px",
    "left": "12px",
    "justifyContent": "space-between",
    "alignItems": "center",
    "borderTop": "none",
    "paddingTop": "24px",
    "paddingLeft": "8px",
    "paddingRight": "8px",
    "backgroundColor": "transparent"
  },
  "button": {
    "padding": "12px 24px",
    "border": "none",
    "borderRadius": "12px",
    "cursor": "pointer",
    "fontSize": "14px",
    "fontWeight": "500",
    "transition": "all 0.3s ease",
    "outline": "none",
    "fontFamily": "inherit",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff",
    "backgroundColor": "#6366f1",
    "color": "#ffffff"
  },
  "primaryButton": {
    "backgroundColor": "#e6e7ee",
    "color": "#5a5a5a",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff, inset 0 0 0 transparent"
  },
  "primaryButton:hover": {
    "boxShadow": "2px 2px 4px #d1d3db, -2px -2px 4px #ffffff, inset 2px 2px 4px #d1d3db"
  },
  "secondaryButton": {
    "backgroundColor": "#e6e7ee",
    "color": "#5a5a5a",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff"
  },
  "dangerButton": {
    "backgroundColor": "#ffeaea",
    "color": "#d63031",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff"
  },
  "disabledButton": {
    "backgroundColor": "#e6e7ee",
    "color": "#b0b0b0",
    "cursor": "not-allowed",
    "boxShadow": "2px 2px 4px #d1d3db, -2px -2px 4px #ffffff",
    "opacity": "0.6"
  },
  "tooltip": {
    "position": "relative",
    "display": "inline-block",
    "cursor": "help",
    "marginLeft": "8px",
    "color": "#5a5a5a"
  },
  "tooltipText": {
    "visibility": "hidden",
    "width": "240px",
    "backgroundColor": "#e6e7ee",
    "color": "#5a5a5a",
    "textAlign": "center",
    "borderRadius": "12px",
    "padding": "8px 12px",
    "position": "absolute",
    "zIndex": "1000",
    "bottom": "125%",
    "left": "50%",
    "marginLeft": "-120px",
    "fontSize": "13px",
    "opacity": "0",
    "transition": "opacity 0.3s",
    "boxShadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  },
  "tooltip_portal": {
    "position": "absolute",
    "left": "0",
    "top": "0",
    "width": "100%",
    "height": "0",
    "pointer-events": "none",
    "z-index": "5000"
  },
  "tooltip_floating": {
    "position": "absolute",
    "z-index": "5010",
    "background": "#e6e7ee",
    "color": "#5a5a5a",
    "padding": "8px 12px",
    "border-radius": "12px",
    "font-size": "13px",
    "box-shadow": "4px 4px 8px #d1d3db, -4px -4px 8px #ffffff",
    "pointer-events": "none",
    "white-space": "pre-line",
    "max-width": "280px",
    "textShadow": "1px 1px 2px rgba(255, 255, 255, 0.8), -1px -1px 2px rgba(0, 0, 0, 0.1)"
  }

Дизайн меняет внешний вид редактируемой формы сразу же:

Можно вызвать демо-версию формы (в меню «Вид/Предварительный просмотр»), и увидеть её так, как она будет выглядеть непосредственно в макросе:

Можно указать необходимые генераторы обработчиков глобальных событий (меню «Правка/События формы»):

Тогда при генерации макроса, будут создаваться специальные fluent методы класса FormMacroManager, позволяющие обрабатывать соответствующие событиям пользовательские функции. А они обязательно понадобятся, если вводимые данные потом нужно проверить перед экспортом или подставить куда то далее в расчеты. В текущей версии макроса, функции обработки редактировать нужно уже в сгенерированном макросе! В Про версии – такие функции можно будет редактировать и в самом плагине.

Можно для табличного редактора осуществить выбор режима запуска формы. Для этого нужно выбрать в дереве структуры формы его верхний узел с названием формы и в параметрах указать режим работы:

В индексе листа мы указываем номер листа, откуда мы будем брать данные, а ниже диапазон области листа, в котором обязательно в самой верхней строке должно быть перечисление имен столбцов, которые используются потом как имена в полях формы!
Так же ниже видны описание размеров формы. При этом надо учитывать, что это общий размер, с кнопками и шапкой модального окна редактора, а не самой области формы! Выше можно сменить название формы и её описание. Внесение изменений производится при нажатии на кнопку «Применить изменения формы»!

Ну и ещё как я упоминал, имеется встроенная справка, с помощью которой, при необходимости, вы можете существенно расширить возможности форм, дополнив сгенерированный плагином код макроса примерами взятыми оттуда, или используя знания о классах описанных там. В справках достаточно подробно описаны  АПИ этих классов, вместе с  примерами кода и данных:

Кстати говоря, окно справки  - это тоже  пример применение классов WindowForm2 совместно с FormMacroManager!

Создание макроса

После того, как вы окончили подготовительную часть и сформировали форму,  ее можно внедрять в макрос, чтобы  продолжить работу с ней уже  непосредственно в редакторе макросов. Для этого в меню «Файл» выбираем «Сохранить», и созданная вами форма внедряется в текущий открытый документ, о чем вам оповестит маленькое окно оповещения! 

Важно! В этой версии 1.0 макрос, непосредственно генерирующий форму, носит одно и тоже имя, и поэтому просто перезатирается при сохранении в документ! Поэтому, если вам важно сохранять свои функции при перезаписях формы, то лучше их вынести в свой отдельный макрос и вызывать их в макрос формы оттуда!


Так же в этом разделе меню вы можете увидеть ещё два пункта: «Экспорт» и «Импорт», с помощью которым вы можете сохранить свои наработки в отдельный внешний файл (пока не сохранятся кастомизация предустановленных стилей, если вы таковую осуществили в  макросе!) и потом их восстановить из него. Иначе говоря, сложные формы созданные вами не пропадут с закрытием документа!

Ну, и ещё есть такой небольшой бонус:  если вы откроете плагин в документе, в котором уже ранее создали форму и сохраняли её данным плагином, то настройки этой формы подгрузятся из макросов автоматически при открытии плагина!

Давайте посмотрим, через штатный плагин редактора макросов, что плагин «ФормаЛаб» сохраняет как макросы в составе документа:

Итак, если вы применяли стили, то у вас добавится 5, если нет, то 4 новых макроса:

  • Start Form 1 - непосредственно макрос точка входа, который создает и конфигурирует окно с формой. Здесь же можно добавлять свои обработчики событий об окончании работы с формой. Помимо создания форму, в нем есть ещё 2 вспомогательные функции getMacros() – ищет макрос в документе по его имени и возвращает его содержимое, если таковой находится, и messageErrorWindow(), который лишь создает системное модальное  окно об ошибке (смотри подробности о таких окнах в прошлой статье). Этот макрос вы смело можете привязывать как макрос запуска к различным графическим элементам в ваших документах.

  • Container Classes Macros - макрос содержащий в специально обработанном виде код обоих классов, требуемых для создания форм

  • Forms Configuration Macros  - макрос возвращающий JSON объект, описывающий сгенерированную форму

  • Style Configuration Macros - макрос возвращающий JSON объект, описывающий кастомную тему формы, если вы её выбирали в плагине. Если стиль формы не выбирался, то этого макроса просто не будет!

  • Class Loader Macros - макрос возвращающий специальный класс ScriptLoader, который извлекает из макроса Container Classes Macros классы WindowForm2 и FormMacroManager, требуемые для создания формы.

Почему так сложно? Про количество макросов я писал ранее – это просто декомпозиция, чтобы в итоге можно было при  работе над макросом сосредоточится на коде, касающемся только той части, которая нужна уже после окончания работы с формой, не листая гигантские простыни кода туда-сюда. К тому же, это ещё и  приходится обычно делать  в неудобном штатном редакторе макросов (но у меня уже есть и более удобный, но пока он ещё в работе и я  предложу его вашему вниманию позже!).

Что касается некоторых странных программных приёмов в Start Form 1, то это просто уловки, которые приходится использовать, чтобы организовать задуманную декомпозицию кода и данных в рамках доступного нам АПИ макросов в Р7/OnlyOffice! Наверное, есть способы и лучше (и я минимум об ещё одном таком знаю), но здесь я пока реализовал всё вот так. В итоге и относительно немного, и как по мне  - вышло достаточно удобно.

Теперь запустим для проверки код макроса StartForm 1:

Код макроса StartForm 1
// Глобальные переменные для хранения классов FormMacroManager и WindowForm2
let Class_FormMacroManager = null;
let Class_WindowForm2= null;//НЕ УДАЛАТЬ! ДОЛЖНА ПРИСУТСТВОВАТЬ ОБЯЗАТЕЛЬНО!!!
let formConfig=eval(getMacros("Forms Configuration Macros").value);
let styleForm=eval(getMacros("Style Configuration Macros").value);
// Загрузка FormMacroManager и WindowForm2  через ScriptLoader
(function() {
    // Получаем ссылки на загруженные скрипты из макросов
    let scriptLibraryJson = eval(getMacros("Container Classes Macros").value);
    let ScriptLoader = eval(getMacros("Class Loader Macros").value);

    // Создаем загрузчик скриптов
    let loaderScripts = new ScriptLoader(scriptLibraryJson);
    
    //Загрузка класса формы в отдельный глобальный объект
    Class_WindowForm2= loaderScripts.executeScript("WindowForm2");
    // Загружаем класс FormMacroManager
    Class_FormMacroManager = loaderScripts.executeScript("FormMacroManager");
    if(Class_WindowForm2&&Class_FormMacroManager){
        console.log("FormMacroManager загружен успешно");
        createForm();        
        Api.asc_calculate(Asc.c_oAscCalculateType.ActiveSheet); 
    }   
})();

/**
 * Создание формы WindowForm2 с помощью FormMacroManager
 */
function createForm() {
    (async function() {
        try {
            // Создаем менеджер форм для режима ввода данных
            const formManager = new Class_FormMacroManager(Class_WindowForm2,Api,Common,{
                mode: 'input',
                windowTitle: 'Ввод ФИО',
                windowWidth: 800,
                windowHeight: 600,
                exportFormat: 'excel',
                documentType: 'excel',                
                language: 'ru',
                                
            });

            // Конфигурируем форму через fluent interface
            formManager
                .setFormConfig(
                    formConfig
                )
            .setStyles(styleForm)
            .setOnClose((data, mode) => {
                console.log('Форма закрыта. Режим:', mode);
                if (data) {
                    console.log('Данные формы:', data);
                }
            });
            // Инициализируем и показываем форму
            await formManager.initialize();
            const window = await formManager.showWindow();
            console.log('Форма ввода данных создана:', formManager.getInfo());

        } catch (error) {
            console.error('Ошибка создания формы ввода данных:', error);
            messageErrorWindow('Ошибка создания формы: ' + error.message);
        }
    })();
}

//Вспомогательная функция загрузки макроса в составе документа, по его имени 
function getMacros(nameMacros)
{
    var macrossesArray=JSON.parse(Api.pluginMethod_GetMacros());
    var finedMacros=macrossesArray.macrosArray.find(findM, nameMacros);
    return finedMacros;  

    function findM(macro) {
        return macro.name === this;
    }
}

//Вывод окна сообщения пользователю
function messageErrorWindow(textMessage){
    Common.UI.error({                
                msg: textMessage,
                width: "auto",
                buttons:['ok']
            }); 
}

Я ввел ФИО искомого работника, и смотрю на результат, получаемый из формы (см код макроса) в логах:

Итоги

Вот так, сравнительно быстро можно создавать формы для ввода, или чтения данных, не дожидаясь, когда появятся штатные способы в макросах. Я, конечно же, в курсе про формы в текстовых документах "Р7". Но, к сожалению, ничего подобного нет в таблицах! И когда появятся (и появятся ли) - не ясно! Да  и в текстовом редакторе, формы не так-то просто использовать, если говорить про автоматизацию.  И вообще, есть много причин, по которым мое решение может оказаться просто проще и удобнее, к тому же оно вот уже сейчас готово к использованию!

На текущий момент написания данный плагин не размещен в магазине плагинов макроса. Во-первых, в нем ещё есть над чем работать, в плане доделок. Во-вторых,  попасть в список разрешенных плагинов этого магазина не так то и легко! Так что, кому нужно прямо сейчас – обращайтесь ко мне. Текущая версия полностью бесплатна. Я гарантирую, что код внутри плагина никуда не лезет и никакую информацию не тырит! Единственный момент – создаваемые макросы триальные и будут работать до весны 2026 года. Далее, будет видно. На подходе платная версия плагина, в которой триальный срок будет либо снят, либо станет существенно дольше. Кроме того там будет генерация форм с помощью ИИ (если у вас есть ключи к таковым) и много чего ещё дополнительного  в плане удобства и простоты создания форм.

Надеюсь, вам было интересно всё, что я изложил в первых трех статьях. Я планирую ещё одну статью по этой теме, но уже в приложении генератора плагинов, использующий родительский  для WindowForm2 класс Wizard (смотри об этом классе  спойлер в начале статьи). А так же, ещё одну типа подведения итогов.  Но её содержимое и сроки будут зависеть уже от обратной связи с вами. Если будут вопросы и мне что сказать по ним, то я, конечно,  её напишу!
Спасибо всем, кто дочитал до этого мест, и надеюсь было не скучно и полезно!
Вы можете меня найти в телеграмме как @shumakov_vitaliy и в новой группе в ВК по вопросам автоматизации в отечественных редакторах

Комментарии (0)