Углубленное изучение АПИ плагинов Р7-Офис
Занятие 4
Создание модальных окон.
Диспетчеризация сообщений
Введение
До версии 7.4 редактора Р7-Офис в плагине не было возможности создавать в одном потоке более одного окна с пользовательским интерфейсом. Для обхода этого ограничения создаются вариации плагина, которые вызываются из основного меню тулбара «Плагины».

Для примера, при необходимости создается дополнительное окно «О программе», отображающее справочную информацию о плагине:

При создании такой вариации в файле конфигурации config.json задается новый элемент в массиве variations с конфигурацией окна.
Пример такого массива в файле config.json с двумя элементами приведен ниже:

"variations" : [
        {
            "description" : "Учебный плагин",
            "url"         : "index.html",

            "icons": [ "logo(64х64).png" ],           
            "isViewer"        : true,
            "EditorsSupport"  : ["cell"],

            "isVisual"        : true,
            "isModal"         : false,
            "isInsideMode"    : true,

            "initDataType"    : "",
            "initData"        : "",
            "isUpdateOleOnResize" : false,
            "buttons"         : []            
        },
        {
            "description" : "About",
            "descriptionLocale": {                
                "ru": "О плагине"
            },
            "url"         : "about.html",

            "icons": [ "logo(64х64).png"],
            
            "isViewer"        : true,
            "EditorsSupport"  : ["cell"],

            "isVisual"     : true,
            "isModal"      : true,
            "isInsideMode" : false,

            "initDataType" : "none",
            "initData"     : "",

            "isUpdateOleOnResize" : false,

            "buttons" : [ { "text": "Ok", "primary": true } ],

            "size" : [400, 250]
        }
    ]
Представление окон задается в файлах HTML. В нашем случае файлы index.html и about.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>About</title>
    <style>
        p, a{
            font-size: 12px;
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        }
    </style>
    <script type="text/javascript" src="../v1/plugins.js"></script>
    <script type="text/javascript" src="../v1/plugins-ui.js"></script>
    <link rel="stylesheet" href="../v1/plugins.css">    
</head>
<body style="padding-left: 20px; padding-right: 20px">
<p style="font-size: 12px">Тестовый плагин</p>

</body>
    <script type="text/javascript">
        window.Asc.plugin.init = function()
        {           
            this.resizeWindow(400, 250, 400, 250, 400, 250);         
        };    
        window.Asc.plugin.button = function(id)
        {
            this.executeCommand("close", "");
        };
    </script>
</html>
Окно будет выглядеть так:
Окна index и aboutостаются независимыми. Они не могут оперативно взаимодействовать. При вызове одного модального окна, второе будет закрыто.

Это неудобно и ограничивает сложность проектов в Р7-Офис. Начиная с версии 7.4 АПИ плагинов Р7-Офис функционально был расширен. Добавлен механизм динамического создания нескольких модальных окон и диспетчеризации сообщений между ними и центральным кодом плагина.
Создание модального окна в плагине
Модифицируем код плагина таким образом, чтобы модальное окно появилось при нажатии на кнопку в основной панели плагина. Для этого в HTML документ базовой панели «index.html» добавим кнопу «Создать окно» с id="btn-2". Используем как образец уже готовое окно «О программе», код которого приведен выше.

Код файла index.html:

<!DOCTYPE html>
<html lang="ru">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">         
        <style type="text/css">
            html, body {
                margin: 0px;
                padding: 0px;
                /* overflow: hidden; */
                width:100%;
                height:100%;
            }
        </style>                
       <script type="text/javascript" src="../v1/plugins.js"></script>   
        <script type="text/javascript" src="code.js"></script>
       <link rel="stylesheet" href="../v1/plugins.css"> 
        <script type="text/javascript" src="code.js"></script>
    </head>
    <body>      
        <div style="width: 100%;">          
            <button id="btn-1" style="width:100%;">Окрасить ячейку</button>
            <button id="btn-2" style="width:100%;">Создать окно</button>            
        </div>
    </body>
</html>
Изменим код скрипта code.js. Для этого в глобальной области добавим переменную modalWindow, проинициализируем ее:

(function(window, undefined){
//Указатель на модальное окно плагина
let modalWindow=null;


Добавим обработчик события click на кнопку с id=”btn-2” в функции window.Asc.plugin.init().

window.Asc.plugin.init = function()
{
//Средствами DOM назначим функцию changeColor на обработку нажатия на кнопку id=btn-1
document.getElementById('btn-1').addEventListener('click', changeColor);
//Средствами DOM назначим функцию changeColor на обработку нажатия на кнопку id=btn-2
document.getElementById('btn-2').addEventListener('click',createWindows);};

Создадим окно с помощью функции createWindows():

function createWindows(){
//Описатель создаваемого окна
let variation={};
//Объект, по которому идёт загрузка файлов плагина
let location = window.location;
//Путь к расположению плагина в файловой системе
let start = location.pathname.lastIndexOf('/') + 1;
//Имя текущего скрипт файла плагина
let file = location.pathname.substring(start);
//Фактически описатель вариации окна "О программе" из config.json
variation = {
description : window.Asc.plugin.tr('About'),
url: location.href.replace(file,"about.html"),
icons : ["logo(64х64).png"],
isViewer: true,
EditorsSupport: ["cell"],
isVisual: true,
isModal: true,
isInsideMode: false,
buttons:[ { "text": "Ok", "primary": true } ],
size: [400, 250]
}
//Если окно не создавалось, то создаём его
if (!modalWindow) {
//функция АПИ, которая делает всю работу по созданию окна плагина
modalWindow = new window.Asc.PluginWindow();
//Присоединяем обработчик различных "оконных" событий для созданного окна
modalWindow.attachEvent("onWindowMessage", function(message) {
//Наша функция по обработке событий
messageHandler(modalWindow, message);
});
}
//Показываем модальное окно пользователю
modalWindow.show(variation);
}
Разберемся с тем, как работает функция создания окна.
1.Вычисляет по определённому алгоритму физический путь к файлу с описателем окна «about.html».

2.В объекте «variation» заполняет сведения о создаваемом окне, повторяя все пункты, которые используются с этой целью в файле конфигурации «config.json». В этом объекте для параметра с именем «url» будет прописан путь, который был вычислен на шаге 1, с заменой имени текущего файла скрипта «code.js», на требуемый нам «about.html».

3.Если модальное окно ранее не создавалось (равенство переменной, отвечающей за объект модальных окон «modalWindow», системной величине неопределённого значения undefined, присвоенной ей при объявлении), функция с помощью функции АПИ window.Asc.PluginWindow() создаёт объект окна.

4.Присоединяет к этому модальному окну обработчик системного события "onWindowMessage". Делается это с помощью системной функции АПИ плагинов "attachEvent()". Подробнее о ней будет рассказано в следующем разделе.

5.Показывает окно с помощью функции объекта modalWindow.show(variation). Создание окна происходит именно в этот момент, для этого этой функции и передаётся объект-описатель окна «variation». Отсюда следует, что любые попытки взаимодействовать с окном до вызова этой функции приведут к ошибкам!

Механизм взаимодействия окон в Р7-Офис, начиная с АПИ 7.4 – Диспетчеризация сообщений
Для работы сложных плагинов необходимо решать дополнительные задачи, такие как конфигурирование плагина, выбор из набора данных, формирование этапов работы и другие.
Для взаимодействия дополнительных окон с основным кодом плагина в Р7-Офис, начиная с АПИ 7.4 введена диспетчеризация сообщений.

Диспетчеризация позволяет обмениваться сообщениями между потоками:
Плагин – Основное окно Редактора Р7-Офис.
Основной модуль плагина – Модальное окно.

Механизм диспетчеризации функционирует с помощью зарегистрированных сообщений.
Часть сообщений (event) прописана в АПИ для определенных функций. В случае обнаружения таких функций при загрузке плагина диспетчер регистрирует их как обработчики событий. Так происходит при обнаружении функции инициализации плагина window.Asc.plugin.init() или window.Asc.plugin.button() – функции обработки событий нажатия описанных в конфигурации плагина кнопок.

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

Пример кода:
//Присоединяем обработчик различных "оконных" событий для созданного окна
modalWindow.attachEvent("onWindowMessage", function(message) {
//Наша функция по обработке событий
messageHandler(modalWindow, message);
});

Используется функция attachEvent() объекта окна с аргументами "onWindowMessage" (первый аргумент) и callback-функция, вызываемая в момент получения окном этого события (второй аргумент).

Рабочий пример универсальной callback-функции диспетчера сообщений в плагине:

//универсальная функция диспетчер сообщений между окнами плагина
function messageHandler(modal, message) {
console.log("messageHandler "+ message.type);
switch (message.type) {
//Метод закрытия модального окна
case "onCancelMethod":
window.Asc.plugin.executeMethod('CloseWindow',[modal.id]);
break;
}
};

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

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

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

<body style="padding-left: 20px; padding-right: 20px">
    <button id="btn-close" style="width:100%;">Закрыть окно</button>
    <p style="font-size: 12px">Тестовый плагин</p>
</body>
2.Добавим обработку нажатия кнопки, с передачей сигнала на закрытие через диспетчеризацию:

<script type="text/javascript">               
        window.Asc.plugin.init = function()        
        {           
            document.getElementById('btn-close').addEventListener('click', function(){
                console.log("send 'onCancelMethod'");
                window.Asc.plugin.sendToPlugin("onWindowMessage", {type: 'onCancelMethod'});   
            });        
        };             
    </script>
3. Изменим обработчик сигнала нажатия на системные кнопки в скрипте базового окна code.js, так как в ином случае плагин будет закрывать окна некорректно. Это произойдет потому, что обработчиком события «button» для всех окон плагина назначается только одна функция в этом файле:

//Обработчик нажатия кнопки крестика закрытия в форме плагина
    window.Asc.plugin.button = function(id, windowId) {        
        if (windowId) {
            switch (id) {
                case -1:
                default:                                        
                    window.Asc.plugin.executeMethod('CloseWindow', [windowId]);
            }
        }       
    };

Запустим наш проект, нажав на кнопку вызова плагина (1). Появится панель плагина. Нажмем кнопку Создать окно на панели плагина (2). Появится наше модальное окно About (3).
Чтобы посмотреть работу нашего механизма передачи сообщений, запустим Консоль отладки (Правая кнопка мыши на панели плагина, пункт Show DevTools, Вкладка Console).
При нажатии кнопки Закрыть окно окна About в консоли появится сообщение «send ‘onCancelMethod’», говорящее о том, что был послан сигнал создания события onCancelMethod базовому скрипту.

Второе сообщение «messageHandler onCancelMethod» говорит о том, что обработчик messageHandler() получил этот сигнал. Модальное окно About будет закрыто, то есть обработка события прошла верно, механизм диспетчеризации работает.

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

Напрямую передавать данные между модальными окнами нельзя, но благодаря встроенному в javascript механизму сохранения объектов в строчном виде JSON формата (strObj = JSON.stringify(obj)) и обратному преобразованию в объектный вид (obj = JSON.parse(strObj)), можно реализовать передачу данных и объектов между основным потоком и модальным окном, а значит и между модальными окнами. Причиной такого решения является невозможность в потоке модального окна использования функции АПИ window.Asc.plugin.callCommand() из-за потери контекста window.

Изменим наш плагин для демонстрации способа передачи данных между потоками.

1.В коде файла «index.html» добавим поле редактора:

<body>      
        <div style="width: 100%;">
            <textarea id ="txtOut" style="height:30px;width: 100%;" class="form-control" ></textarea>           
            <button id="btn-1" style="width:100%;">Окрасить ячейку</button>
            <button id="btn-2" style="width:100%;">Создать окно</button>            
        </div>
    </body>
2.В коде файла «code.js» изменим обработчики:

//универсальная функция диспетчер сообщений между окнами плагина
    function messageHandler(modal, message) {
        console.log("messageHandler "+ message.type);           
        switch (message.type) {
            case "onSalut":                     
                let txt=document.getElementById('txtOut').innerText=
                "Привет из модального окна"+"\n Полученные данные: \n"+message.data;
                break;                
            //Метод закрытия модального окна         
            case "onCancelMethod":
                window.Asc.plugin.executeMethod('CloseWindow',[modal.id]);
                break;          
        }
    };

3.Добавим кнопку пересылки данных для окна «About.html» и обработчик её нажатия, в который мы отошлём данные:

<script type="text/javascript">               
        window.Asc.plugin.init = function()        
        {           
            document.getElementById('btn-close').addEventListener('click', function(){
                console.log("send 'onCancelMethod'");
                window.Asc.plugin.sendToPlugin("onWindowMessage", {type: 'onCancelMethod'});   
            });
            document.getElementById('btn-send').addEventListener('click', function(){
                console.log("send 'onCancelMethod'");
                window.Asc.plugin.sendToPlugin("onWindowMessage",
                 {type: 'onSalut',data:JSON.stringify({data1:"Data 1",data2:"2",})});   
            });
        };             
    </script>   
</head>
<body style="padding-left: 20px; padding-right: 20px">
    <button id="btn-close" style="width:100%;">Закрыть окно</button>
    <button id="btn-send" style="width:100%;">Послать сообщение</button>
    <p style="font-size: 12px">Тестовый плагин</p>
</body>
Запустим плагин и отошлем данные из окна About в основной поток плагина.
Запрос и получение данных из основного (базового) потока в модальное окно.
Как видно из примера в предыдущем разделе, сообщение посылается и обрабатывается без закрытия потока модального окна, следовательно, должен быть механизм запроса и получения данных из основного потока для открытого модального окне. Такой механизм разработчиками был внедрён. Он сложнее и требует для получения сообщения регистрации обработчика сообщений в скрипте модального окна:

window.Asc.plugin.attachEvent("onGetWindowUserData",function(data){});

Событие должно быть не onWindowMessage, а пользовательского типа. Нужно в основном потоке (файл code.js) использовать специальный метод, привязанный к модальному окну, с которым надо связаться.

modalWindows.command(“onGetWindowUserData”, sendData);

Эту команду нужно прописать в обработчике сообщения onSendData, которое модальное окно пошлет для запроса данных:

function messageHandler(modal, message) {
console.log("messageHandler "+ message.type);
switch (message.type) {
case "onSendData":
modal.command(“onGetWindowUserData”,{ourSendData:”Data to send”});
break;
….
}
};

Модальное окно должно послать вызов:

window.Asc.plugin.sendToPlugin("onWindowMessage", {type: '"onSendData"'});

В целом схема такая:

1.Из модального окна About.html, с помощью window.Asc.plugin.sendToPlugin("onWindowMessage", {type: '"onSendData"'}); отсылается сообщение в базовый поток «code.js».

2.Обработчик событий messageHandler(modal, message), получает сообщение, находит что сообщение типа "onSendData" имеется в списке обрабатываемых им сообщений. Обработчик обработает требуемые данные (например, из документа редактора). Но для простоты, пусть он просто отошлет статичные данные.

3.Отсылка данных осуществляется командой modal.command() объекта модального окна «modal», чей заголовок идёт как первый аргумент в обработчике сообщений messageHandler(modal, message). Повторим код, приведенный выше:

modal.command(“onGetWindowUserData”,{ourSendData:”Data to send”});

4.Если в модальном окне уже зарегистрирован обработчик сигнала onGetWindowUserData (с помощью window.Asc.plugin.attachEvent( "onGetWindowUserData",function(data){}); ). Второй аргумент - callback функция, вызываемая при получении зарегистрированного сообщения onGetWindowUserData и имеющая аргумент data с данными из аргумента message в messageHandler(modal, message).

Схема позволяет осуществлять обмен информацией и командами между модальным окном и основным потоком. При этом модальное окно не закрывается. Никакие дополнительные механизмы, такие как localStorage, не используются.

Приложения
1.Код файла config.json

{
    "name" : "Плагин окрашивания ячейки",
    "guid" : "asc.{6401CE6B-3E19-45E1-9352-BFCF41989AA5}",
    "version": "0.0.1",

    "variations" : [
        {
            "description" : "Учебный плагин",
            "url"         : "index.html",

            "icons": [ "logo(64х64).png" ],                      
            "EditorsSupport"  : ["cell"],           
            
            "isViewer"        : true,
            "isVisual"        : true,
            "isModal"         : false,
            "isInsideMode"    : true,

            "initDataType"    : "",
            "initData"        : "",

            "isUpdateOleOnResize" : false,

            "buttons"         : []            
        }
    ]
}
2.Код файла index.html:

<!DOCTYPE html>
<html lang="ru">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> 
        
        <style type="text/css">
            html, body {
                margin: 0px;
                padding: 0px;
                /* overflow: hidden; */
                width:100%;
                height:100%;
            }
        </style>                
        <script src="../v1/plugins.js" type="text/javascript" ></script>
        <script src="../v1/plugins-ui.js" type="text/javascript"></script>      
        <script src="code.js" type="text/javascript"></script>
        <link href="../v1/plugins.css" rel="stylesheet">
    </head>
    <body>      
        <div style="width: 100%;">
            <textarea id ="txtOut" style="height:100px;width: 100%;" class="form-control" ></textarea>          
            <button id="btn-1" style="width:100%;">Окрасить ячейку</button>
            <button id="btn-2" style="width:100%;">Создать окно</button>            
        </div>
    </body>
</html>
3.Код файла code.js:


(function(window, c=undefined){
    //Указатель на модальное окно плагина
    let modalWindow=null;
    //Функция API вызываемая редактором при инициализации плагина пользователем
    window.Asc.plugin.init = function()
    {
        //Средствами DOM назначим функцию changeColor на обработку нажатия на кнопку id=btn-1
        document.getElementById('btn-1').addEventListener('click', changeColor);
        //Средствами DOM назначим функцию changeColor на обработку нажатия на кнопку  id=btn-2
        document.getElementById('btn-2').addEventListener('click', createWindows);  
    };
    
    //Обработчки нажатия кнопки крестика закрытия в форме плагина
    window.Asc.plugin.button = function(id, windowId) {     
        
        if (windowId) {
            switch (id) {
                case -1:
                default:                                        
                    window.Asc.plugin.executeMethod('CloseWindow', [windowId]);
            }
        }       
    };

    function createWindows(){
        //Объект, по которому идёт загрузка файлов плагина
        let location  = window.location;
        //Путь к расположению плагина в файловой системе
        let start = location.pathname.lastIndexOf('/') + 1;
        //Имя текущего скрипт файла плагина 
        let file = location.pathname.substring(start);
        
        //Фактически описатель вариации окна "О программе" из config.json
        let variation = {
            description : 'About',
            url: location.href.replace(file,'about.html'),  
            icons : ['logo(64х64).png'],            
            EditorsSupport: ["cell"],   
            isVisual: true,
            isModal: true,              
            buttons:[ { "text": "Ok", "primary": true }],   
            size: [400, 250]
        };

        //Если окно не создавалось, то создаём его
        if (!modalWindow) {
            //функция АПИ, которая делает всю работу по созданию окна плагина
            modalWindow = new window.Asc.PluginWindow();
            //Присоединяем обработчик различных "оконных" событий для созданного окна
            modalWindow.attachEvent("onWindowMessage", function(message) {
            //Наша функция по обработке событий
                messageHandler(modalWindow, message);
            });
        }

        //Показываем модальное окно пользователю
        modalWindow.show(variation);
    }

    //универсальная функция диспетчер сообщений между окнами плагина
    function messageHandler(modal, message) {
        console.log("messageHandler "+ message.type);           
        switch (message.type) {
            case "onSalut":                     
                let txt=document.getElementById('txtOut').innerText=
                "Привет из модального окна"+"\n Полученные данные: \n"+message.data;
                break;
                
            //Метод закрытия модального окна         
            case "onCancelMethod":
                window.Asc.plugin.executeMethod('CloseWindow',[modal.id]);
                break;          
        }
    };

    //Сменить цвет ячейки (ячеек), которую перед вызовом функции выделил пользователь
    function changeColor(){
        //Создание "песочницы" js, в поторой происходит работа с документом
        window.Asc.plugin.callCommand(function() {          
            let oWorksheet =Api.GetActiveSheet();//Получить активный лист           
            let newColor=Api.CreateColorFromRGB(240, 240, 240);//Создать серый цвет
            if(oWorksheet!=undefined){
                let oActiveCell = oWorksheet.GetActiveCell();//Получить активную ячейку
                if(oActiveCell!=undefined){//Залить активную ячейку, если она существует
                    oActiveCell.SetFillColor(newColor);
                }
            }
        }, undefined,true);
    };    
})(window, undefined);
4.Код файла about.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>About</title>
    <style>
        p, a{
            font-size: 12px;
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        }
    </style>
    <script type="text/javascript" src="../v1/plugins.js"></script>    
    <link rel="stylesheet" href="../v1/plugins.css"> 
    <script type="text/javascript">               
        window.Asc.plugin.init = function()        
        {           
            document.getElementById('btn-close').addEventListener('click', function(){
                console.log("send 'onCancelMethod'");
                window.Asc.plugin.sendToPlugin("onWindowMessage", {type: 'onCancelMethod'});   
            });
            document.getElementById('btn-send').addEventListener('click', function(){
                console.log("send 'onCancelMethod'");
                window.Asc.plugin.sendToPlugin("onWindowMessage",
                 {type: 'onSalut',data:JSON.stringify({data1:"Data 1",data2:"2",})});   
            });
        };             
    </script>   
</head>
<body style="padding-left: 20px; padding-right: 20px">
    <button id="btn-close" style="width:100%;">Закрыть окно</button>
    <button id="btn-send" style="width:100%;">Послать сообщение</button>
    <p style="font-size: 12px">Тестовый плагин</p>
</body>
    
</html>