Как стать автором
Обновить

Google Docs Add-on. Расширяем возможности редактора Google Docs

Разработка веб-сайтов *JavaScript *
Из песочницы
На днях Google анонсировал выход новой платформы, позволяющей разработчикам создавать приложения, работающие внутри Google Docs и расширяющие базовый функционал Google Docs редактора.
Разберемся что это, как это работает и напишем небольшое приложение которое позволит нам переводить текст документа не выходя из Google Docs.

Немного теории.

Google Docs Аdd-on — приложение, написанное на javascript'е, существующее и работающее в Google Drive (сейчас пока только для Google Docs), имеющее свой UI ('sidebar' справа от документа или модальное окно поверх), расширяющее функционал Google Docs редактора. Google Docs Аdd-on является следующим шагом в развитии Google Apps Script. По сути это Google Apps Script проект с возможностью распространения и установки его другими пользователями Google Docs.

Существует несколько вида Google Apps Script проектов, основные из них — отдельный файл-проект в Google Drive (В Google Drive вы видите его как отдельный файл, 'standalone script') или проект с документом-контейнером ('container-bounds script'). Его вы можете увидеть открыв документ-контейнер, меню 'Tools' -> 'Script editor'. Google Docs Аdd-on — как раз второй вариант, для его создания необходим документ-контейнер, который вы будете использовать для тестирования и отладки вашего приложения.

Распространение

Распространяются Google Docs Аdd-on через Chrome Web store (хотя и не являются расширением браузера). Перед добавлением вашего add-on'a в Chrome Web Store необходимо пройти review со стороны Google. Для пользователей Google Docs все add-on'ы доступны для установки через новый пункт меню — 'Add-ons' -> 'Get Add-ons'.

Среда разработки

Для Google Apps Script проектов Google предоставляет среду разработки, интегрированную в Google Drive, с возможностью запуска, отладки и автодополнения кода. Есть ли возможность использовать другую среду разработки и хранить код не в Google Drive, а в CVS? Это позволяло бы работать над проектом нескольким разработчикам одновременно и поддерживать версионирование. Для 'standalone' проектов такая возможность есть, для 'сontainer-bounds' — к сожалению пока нет. Поэтому для разработки Google Docs Add-on'a приходится использовать интегрированную среду разработки. Вот пример python скрипта, который позволяет «выкачать» 'standalone' проект на файловую систему и обновить его в Google Drive после изменения.

Что внутри

Немного подробнее о том, как работает Google Apps Script. Файлы проекта содержат «серверную» (.gs) и «клиентскую» (.html) части приложения. «Серверная» часть — набор javascript функций, которые могут обращаться к сторонним сервисам, различным Google API (Drive, GMail, Calendar), в том числе к API документа, в котором установлен данный add-on.
Также '.gs' файлы содержат предопределенные функции, которые вызываются когда пользователь устанавливает/открывает add-on, что позволяет создавать UI вашего приложения внутри стандартного редактора Google Docs. В свою очередь «клиесткая часть» представляет собой стандартные html файлы, с возможностью вызова любой функции из любого «серверного» файла. А также, естественно, с возможностью подключения сторонних CSS/javascript файлов.

Пример

Напишем приложение, которое будет переводить выделенный текст в документе с английского на русский язык. Создаем новый Google Docs документ в Google Drive, открываем меню 'Tools' -> 'Script editor'. В появившемся диалоге выбираем 'Create script for -> Document'. После этого вы увидите ту самую интегрированную среду разработки. В проекте по умолчанию создается файл 'Code.gs', содержащий пример приложения от Google. Нам он пока не нужен, поэтому я сразу предлагаю очистить его содержимое, а сам файл переименовать в 'Server.gs'. Также лучше сразу переименовать проект из 'Untitled project', скажем в 'Translate example'.

Итак, в 'Server.gs' мы определим функции, которые будут создавать UI нашего add-on'a. Для этого достаточно объявить функцию 'onOpen'.
function onOpen() {
  DocumentApp.getUi().createAddonMenu()
    .addItem('Translate', 'openSidebar')
    .addToUi();
}

function openSidebar( ) {}


Теперь, при открытии документа, в котором установлен данный add-on, в специальном меню 'Add-ons' появиться пункт 'Translate'. Нажатие на данный пункт меню будет вызывать функцию 'openSidebar', которая пока ничего не делает. Чтобы проверить это, в среде разработки выберите функцию «onOpen» и нажмите 'Run'


Теперь в вашем документе появился пункт меню 'Add-ons' -> 'Translate example' (название вашего проекта) -> 'Translate'

Займемся разработкой UI для нашего add-on'a. Так как UI add-on'a представляет собой html страницу, то есть смысл сразу разделить HTML разметку, CSS и javascript код на разные файлы. Для этого мы создадим файлы 'Sidebar.html', 'Styles.html', 'Scripts.html' и 'Content.html'. 'Sidebar.html' будет файлом-темплейтом, включающем в себя остальные файлы.



UI всех add-on'ов должен следовать определенному style guide, поэтому Google предоставляет специальный CSS файл с предопределенными стилями, ссылку на который вы видите в примере.

Открываем 'Content.html' и 'Styles.html' и создаем разметку
<div class="content">
  <p>Select text and click 'Translate'</p>
  <button class="action btn-block">Translate</button>
  <p class="result"></p>
</div>

и стили
<style>
  .content {
    padding: 20px;
    margin : 20px;
  }

  .btn-block {
     display: block;
     width: 100%;
     padding-left: 0;
     padding-right: 0;
  }
</style>

а в 'Server.gs' дописываем тело функции 'openSidebar'
function onOpen() {
  DocumentApp.getUi().createAddonMenu()
    .addItem('Translate', 'openSidebar')
    .addToUi();
}

function openSidebar( ) {
  var html = HtmlService.createTemplateFromFile('Sidebar')
    .evaluate()
    .setSandboxMode(HtmlService.SandboxMode.NATIVE)
    .setTitle('Translate example')
    .setWidth(300);
  DocumentApp.getUi().showSidebar(html);
}

Теперь по нажатию пункт меню «Translate» в документе, слева, откроется 'sidebar'.

Осталось добавить логику для перевода текста. Для этого мы будем использовать API различных Google сервисов, доступных нам в '.gs' файлах и позволяющих находить выделенный в документе текст и переводить его на выбранный язык.
'Server.gs'
function translate(pollingCounter) {
  var text = getSelectedText(getSingleSeletetedElement());
  if (text) {
    return LanguageApp.translate(text, 'en', 'ru');
  }
}

function getSelectedText(rangeElement) {
  if (!rangeElement) { return; }
  
  var element = rangeElement.getElement();
  var from = rangeElement.getStartOffset();
  var to = rangeElement.getEndOffsetInclusive() + 1;
  if (element.getType() === DocumentApp.ElementType.TEXT || element.getType() === DocumentApp.ElementType.PARAGRAPH) {
    var text = element.getText();
    return text.substring(from, to);
  }
}

function getSingleSeletetedElement() {
  var selection = DocumentApp.getActiveDocument().getSelection();
  if (selection) {
    var elements = selection.getSelectedElements();
    if (elements.length === 1) {
      return elements[0];
    }
  }
}

Функция 'translate' ищет выделенный элемент в документе, и если это текст или текстовый блок — получает выделенную часть текста и переводит ее с английского на русский язык. Теперь нам нужно как-то вызвать эту функцию из нашего UI. Для этого javascript код, запущенный в 'sidebar' имеют доступ к глобальному объекту 'google.script.run', позволяющему вызвать любую функцию из любого '.gs' файла.
'Scripts.html'
  $(function() {
    
    $('.action').click(function() {
       google.script.run.withSuccessHandler(function(text) {
         if (text) {
           $('.result').text('Result: ' + text);
         } else {
           $('.result').text('Please, select text to translate');
         }
       }).translate();
    });   
  });

Готово, теперь наш add-on может переводить выделенный текст по нажатию на клавишу 'Translate'.

С ходу можно внести 2 улучшения. Во-первых, при открытии 'sidebar', пока загружаются все необходимые для него файлы, было бы неплохо добавить какой-нибудь индикатор загрузки. Создадим файл 'Loading.html', добавим ссылку на него в наш 'Sidebar.html', а основное содержимое спрячем до полной загрузки всех ресурсов. Как только все готово — мы прячем индикатор и показываем наш UI.
'Loading.html'
<div class="app-loading" style="text-align:center">
      <br><br><br><br><br><br>      
      Loading...
</div>

Content.html
<div class="content" style="display:none">
  <p>Select text and click 'Translate'</p>
  <button class="action btn-block">Translate</button>
  <p class="result"></p>
</div>

Sidebar.html
<!-- styles -->
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<?!= HtmlService.createHtmlOutputFromFile('Styles').getContent(); ?>

<!-- layout -->
<?!= HtmlService.createHtmlOutputFromFile('Loading').getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile('Content').getContent(); ?>

<!--3rd party scripts -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

<!-- application scripts -->
<?!= HtmlService.createHtmlOutputFromFile('Scripts').getContent(); ?>

и наконец в Scripts.html
  $(function() {
    $('.app-loading').hide();
    $('.content').show();
    $('.action').click(function() {
       google.script.run.withSuccessHandler(function(text) {
         if (text) {
           $('.result').text('Result: ' + text);
         } else {
           $('.result').text('Please, select text to translate');
         }
       }).translate();
    });   
  });

А во-вторых, было бы неплохо автоматически получать перевод выделенного текста из документа, без нажатия кнопки 'Translate'. Для решения этой задачи нужно придумать как обойти одно из ограничений, существующее сейчас в Google Docs Аdd-on — нет event модели, которая позволяла бы Google Script Аdd-on'у реагировать на действия пользователя в документе. Нам прийдется имитировать интерактивность add-on'a используя long polling — постоянные запросы со стороны UI, которые будут вынуждать наш ' Server.gs' проверять документ на наличие выделения и переводить текст. Для этого напишем в 'Script.html' функцию, которая будет запускать 2 процесса с определенным интервалом. Первый процесс будет вызывать 'translate' функцию и просто сохранять результат перевода. Второй процесс будет проверять на наличие результат перевода и обновлять UI.
'Scripts.html'
  var LongPollingManager = (function() {

    var process = function(options) {
      
      var pollingTimeout = 1500,
        notificationTimeout = 1500,
        pollingCounter = 0,
        pollingStopped = false,
        pollingResult,
        pollingProcess,
        notificationProcess;
 
      var start = function() {
        pollingStopped = false;
        pollingProcess = setInterval(function() {
          if (pollingStopped) { return; }
          pollingCounter++;
          google.script.run.withSuccessHandler(function(response) {
            if (pollingStopped) { return; }
            if (!pollingResult || response.pollingCounter > pollingResult.pollingCounter) {
              pollingResult = response;
            }
          })[options.method](pollingCounter);
        }, pollingTimeout);
         
        setTimeout(function() {
          notificationProcess = setInterval(function() {
            if (pollingStopped) { return; }
            options.notification(getLastResult());
          }, notificationTimeout);
        }, notificationTimeout/2); 
        
      };

      var getLastResult = function() {
        return pollingResult;
      };

      var stop = function() {
        pollingResult = null;
        pollingStopped = true;
        clearInterval(pollingProcess);
        clearInterval(notificationProcess);
      };
                        
      return {
        start: start,
        stop : stop,
        isActive: function() { return !pollingStopped; },
        getLastResult : getLastResult
      };;
    };
    
    return {
      process: process
    };

  })();
  
  var showResult = function(result) {
    if (result && result.text) {
      $('.result').text('Result: ' + result.text);
     } else {
       $('.result').text('');
     }
  };
  
  $(function() {
    $('.loading').hide();
    $('.content').show();
    
    $('.action').click(function() {
       google.script.run.withSuccessHandler(function(text) {
         if (text) {
           $('.result').text('Result: ' + text);
         } else {
           $('.result').text('Please, select text to translate');
         }
       }).translate();
    });
    
    LongPollingManager.process({
      method: 'translate',
      notification: showResult
    }).start();
   
  });

'Server.gs'
function translate(pollingCounter) {
  var result = { pollingCounter : pollingCounter };
  var text = getSelectedText(getSingleSeletetedElement());
  if (text) {
    result.text = LanguageApp.translate(text, 'en', 'ru');
  }
  return result;
}


Готово, теперь наш add-on автоматически переводит выделенный текст.
Документ + код проекта на Google Drive
Код на github



Заключение

Итак, Google Docs add-on — новая платформа, позволяющая создавать приложения, расширяющие возможности базового редактора Google Docs.
Плюсы и минусы, на мой взгляд:
  • + Огромная аудитория Google Docs пользователей.
  • + Разработка приложения полностью на javascript.
  • + Доступ к многочисленным Google API и любому стороннему сервису (HTTP запросы).
  • — Разработка проекта внутри Google Docs, нет возможности вести разработку локально, трудно разделить работу между несколькими разработчиками.
  • — Отсутствие интерактивного взаимодействия с документом (нет возможности получать оповещения о каких-либо действия пользователя в документе.


P. S.

Я думаю что в соперничестве MS Office — Google Drive, Google решил сделать ставку на простоту, удобство и сообщество разработчиков. Поясню на примере MS Word vs Goodle Docs. MS Word имеет огромное количество доступных пользователю функций (и как следствие — перегруженный и сложный UI) и не имеет открытого API. Google Docs — наоборот, обладает минимально необходимым набором инструментов и открытым API. Теперь любую недостающую вам функцию вы можете найти в галлерее Google Docs add-on'ов или написать сами. А пользователь может добавить в свой документ только нужные ему функции.
Теги: GoogleGoogle Docs Add-onGoogle Apps ScriptJavascript
Хабы: Разработка веб-сайтов JavaScript
Всего голосов 28: ↑27 и ↓1 +26
Комментарии 4
Комментарии Комментарии 4

Похожие публикации

Лучшие публикации за сутки